loading
Generated 2025-10-13T07:18:03+00:00

All Files ( 84.86% covered at 41.49 hits/line )

150 files in total.
5204 relevant lines, 4416 lines covered and 788 lines missed. ( 84.86% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/log_struct.rb 86.49 % 72 37 32 5 2.92
lib/log_struct/boot_buffer.rb 100.00 % 28 15 15 0 3.07
lib/log_struct/concerns/configuration.rb 82.61 % 256 138 114 24 108.14
lib/log_struct/concerns/error_handling.rb 86.49 % 90 37 32 5 2.73
lib/log_struct/concerns/logging.rb 100.00 % 45 21 21 0 2.95
lib/log_struct/config_struct/error_handling_modes.rb 100.00 % 25 8 8 0 2.00
lib/log_struct/config_struct/filters.rb 100.00 % 98 26 26 0 124.27
lib/log_struct/config_struct/integrations.rb 100.00 % 98 26 26 0 5.92
lib/log_struct/configuration.rb 100.00 % 72 28 28 0 512.54
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 % 61 30 30 0 2.00
lib/log_struct/enums/level.rb 100.00 % 66 43 43 0 105.49
lib/log_struct/enums/log_field.rb 100.00 % 165 111 111 0 2.00
lib/log_struct/enums/source.rb 100.00 % 29 16 16 0 2.00
lib/log_struct/formatter.rb 92.16 % 236 102 94 8 421.40
lib/log_struct/handlers.rb 100.00 % 27 7 7 0 2.29
lib/log_struct/hash_utils.rb 100.00 % 21 10 10 0 6.30
lib/log_struct/integrations.rb 94.74 % 86 57 54 3 2.11
lib/log_struct/integrations/action_mailer.rb 100.00 % 57 27 27 0 5.30
lib/log_struct/integrations/action_mailer/error_handling.rb 84.85 % 259 99 84 15 1.98
lib/log_struct/integrations/action_mailer/event_logging.rb 92.16 % 116 51 47 4 4.51
lib/log_struct/integrations/action_mailer/metadata_collection.rb 90.91 % 72 33 30 3 7.91
lib/log_struct/integrations/active_job.rb 100.00 % 38 17 17 0 2.12
lib/log_struct/integrations/active_job/log_subscriber.rb 43.64 % 108 55 24 31 0.87
lib/log_struct/integrations/active_model_serializers.rb 94.44 % 49 18 17 1 1.67
lib/log_struct/integrations/active_record.rb 92.68 % 288 123 114 9 11.80
lib/log_struct/integrations/active_storage.rb 39.13 % 133 46 18 28 0.74
lib/log_struct/integrations/ahoy.rb 95.24 % 53 21 20 1 1.52
lib/log_struct/integrations/carrierwave.rb 45.95 % 106 37 17 20 0.97
lib/log_struct/integrations/dotenv.rb 64.97 % 278 157 102 55 1.08
lib/log_struct/integrations/good_job.rb 53.13 % 109 32 17 15 1.22
lib/log_struct/integrations/good_job/log_subscriber.rb 98.53 % 178 68 67 1 1.60
lib/log_struct/integrations/good_job/logger.rb 100.00 % 67 23 23 0 3.43
lib/log_struct/integrations/host_authorization.rb 62.86 % 93 35 22 13 1.31
lib/log_struct/integrations/integration_interface.rb 100.00 % 21 8 8 0 2.25
lib/log_struct/integrations/lograge.rb 69.39 % 120 49 34 15 1.65
lib/log_struct/integrations/puma.rb 56.12 % 477 237 133 104 1.46
lib/log_struct/integrations/rack_error_handler.rb 100.00 % 32 14 14 0 2.14
lib/log_struct/integrations/rack_error_handler/middleware.rb 44.00 % 177 50 22 28 0.92
lib/log_struct/integrations/shrine.rb 25.71 % 97 35 9 26 0.57
lib/log_struct/integrations/sidekiq.rb 58.82 % 39 17 10 7 1.29
lib/log_struct/integrations/sorbet.rb 92.68 % 97 41 38 3 2.51
lib/log_struct/log.rb 100.00 % 42 15 15 0 4.13
lib/log_struct/log/action_mailer.rb 100.00 % 53 20 20 0 2.35
lib/log_struct/log/action_mailer/delivered.rb 100.00 % 64 42 42 0 1.81
lib/log_struct/log/action_mailer/delivery.rb 100.00 % 64 42 42 0 1.81
lib/log_struct/log/action_mailer/error.rb 100.00 % 72 48 48 0 2.04
lib/log_struct/log/active_job.rb 90.00 % 51 20 18 2 1.80
lib/log_struct/log/active_job/enqueue.rb 76.92 % 61 39 30 9 1.54
lib/log_struct/log/active_job/finish.rb 75.61 % 63 41 31 10 1.51
lib/log_struct/log/active_job/schedule.rb 76.92 % 61 39 30 9 1.54
lib/log_struct/log/active_job/start.rb 75.61 % 63 41 31 10 1.51
lib/log_struct/log/active_model_serializers.rb 100.00 % 54 34 34 0 1.82
lib/log_struct/log/active_storage.rb 89.47 % 42 19 17 2 1.79
lib/log_struct/log/active_storage/delete.rb 86.21 % 49 29 25 4 1.72
lib/log_struct/log/active_storage/download.rb 80.00 % 57 35 28 7 1.60
lib/log_struct/log/active_storage/exist.rb 83.87 % 53 31 26 5 1.68
lib/log_struct/log/active_storage/metadata.rb 83.87 % 53 31 26 5 1.68
lib/log_struct/log/active_storage/stream.rb 83.87 % 53 31 26 5 1.68
lib/log_struct/log/active_storage/upload.rb 75.61 % 63 41 31 10 1.51
lib/log_struct/log/active_storage/url.rb 83.87 % 53 31 26 5 1.68
lib/log_struct/log/ahoy.rb 100.00 % 50 30 30 0 1.90
lib/log_struct/log/carrierwave.rb 90.48 % 56 21 19 2 1.81
lib/log_struct/log/carrierwave/delete.rb 75.61 % 61 41 31 10 1.51
lib/log_struct/log/carrierwave/download.rb 72.34 % 69 47 34 13 1.45
lib/log_struct/log/carrierwave/upload.rb 70.59 % 73 51 36 15 1.41
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.04
lib/log_struct/log/dotenv/restore.rb 88.89 % 48 27 24 3 1.78
lib/log_struct/log/dotenv/save.rb 88.89 % 48 27 24 3 1.78
lib/log_struct/log/dotenv/update.rb 100.00 % 48 27 27 0 2.04
lib/log_struct/log/error.rb 100.00 % 55 33 33 0 3.03
lib/log_struct/log/good_job.rb 100.00 % 50 21 21 0 2.24
lib/log_struct/log/good_job/enqueue.rb 100.00 % 63 41 41 0 2.56
lib/log_struct/log/good_job/error.rb 100.00 % 71 49 49 0 2.63
lib/log_struct/log/good_job/finish.rb 100.00 % 67 45 45 0 2.36
lib/log_struct/log/good_job/log.rb 100.00 % 67 45 45 0 7.42
lib/log_struct/log/good_job/schedule.rb 100.00 % 65 43 43 0 2.07
lib/log_struct/log/good_job/start.rb 100.00 % 65 43 43 0 2.12
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 86.52
lib/log_struct/log/puma.rb 100.00 % 10 2 2 0 2.00
lib/log_struct/log/puma/shutdown.rb 90.00 % 53 30 27 3 1.80
lib/log_struct/log/puma/start.rb 76.09 % 69 46 35 11 1.52
lib/log_struct/log/request.rb 100.00 % 77 54 54 0 2.63
lib/log_struct/log/security.rb 89.47 % 50 19 17 2 1.79
lib/log_struct/log/security/blocked_host.rb 74.07 % 80 54 40 14 1.48
lib/log_struct/log/security/csrf_violation.rb 79.55 % 70 44 35 9 1.59
lib/log_struct/log/security/ip_spoof.rb 77.08 % 74 48 37 11 1.54
lib/log_struct/log/shrine.rb 100.00 % 13 5 5 0 2.00
lib/log_struct/log/shrine/delete.rb 86.21 % 50 29 25 4 1.72
lib/log_struct/log/shrine/download.rb 83.87 % 52 31 26 5 1.68
lib/log_struct/log/shrine/exist.rb 83.87 % 52 31 26 5 1.68
lib/log_struct/log/shrine/metadata.rb 83.87 % 52 31 26 5 1.68
lib/log_struct/log/shrine/upload.rb 78.38 % 58 37 29 8 1.57
lib/log_struct/log/sidekiq.rb 81.25 % 52 32 26 6 1.63
lib/log_struct/log/sql.rb 100.00 % 73 51 51 0 4.86
lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb 77.78 % 47 18 14 4 2.06
lib/log_struct/multi_error_reporter.rb 84.62 % 207 104 88 16 2.12
lib/log_struct/param_filters.rb 90.48 % 132 63 57 6 414.54
lib/log_struct/rails_boot_banner_silencer.rb 93.10 % 116 58 54 4 2.07
lib/log_struct/railtie.rb 55.26 % 85 38 21 17 1.11
lib/log_struct/semantic_logger/color_formatter.rb 78.57 % 209 84 66 18 17.27
lib/log_struct/semantic_logger/concerns/log_methods.rb 95.16 % 100 62 59 3 65.24
lib/log_struct/semantic_logger/formatter.rb 96.30 % 96 27 26 1 244.63
lib/log_struct/semantic_logger/logger.rb 83.33 % 160 54 45 9 4.11
lib/log_struct/semantic_logger/setup.rb 80.95 % 239 63 51 12 1.87
lib/log_struct/shared/add_request_fields.rb 64.71 % 28 17 11 6 1.29
lib/log_struct/shared/interfaces/additional_data_field.rb 100.00 % 22 10 10 0 2.00
lib/log_struct/shared/interfaces/common_fields.rb 100.00 % 39 20 20 0 2.00
lib/log_struct/shared/interfaces/public_common_fields.rb 100.00 % 29 14 14 0 2.07
lib/log_struct/shared/interfaces/request_fields.rb 100.00 % 39 19 19 0 2.00
lib/log_struct/shared/merge_additional_data_fields.rb 100.00 % 27 15 15 0 116.27
lib/log_struct/shared/serialize_common.rb 100.00 % 64 31 31 0 401.16
lib/log_struct/shared/serialize_common_public.rb 95.65 % 44 23 22 1 2.30
lib/log_struct/sorbet.rb 100.00 % 13 2 2 0 2.00
lib/log_struct/sorbet/serialize_symbol_keys.rb 83.33 % 23 12 10 2 1.67
lib/log_struct/string_scrubber.rb 100.00 % 84 39 39 0 1817.41
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/logging_controller.rb 70.21 % 147 47 33 14 1.02
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 % 19 8 8 0 2.25
rails_test_app/logstruct_test_app/app/models/user.rb 72.73 % 23 11 8 3 0.73
rails_test_app/logstruct_test_app/config/application.rb 100.00 % 37 12 12 0 1.00
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 100.00 % 36 22 22 0 1.00
rails_test_app/logstruct_test_app/config/routes.rb 100.00 % 17 10 10 0 1.20
rails_test_app/logstruct_test_app/test/integration/action_mailer_id_mapping_test.rb 100.00 % 101 44 44 0 1.48
rails_test_app/logstruct_test_app/test/integration/active_storage_test.rb 98.86 % 208 88 87 1 1.58
rails_test_app/logstruct_test_app/test/integration/boot_logs_integration_test.rb 97.62 % 83 42 41 1 1.33
rails_test_app/logstruct_test_app/test/integration/dotenv_integration_test.rb 95.24 % 43 21 20 1 1.10
rails_test_app/logstruct_test_app/test/integration/host_authorization_test.rb 92.73 % 123 55 51 4 1.07
rails_test_app/logstruct_test_app/test/integration/logging_integration_test.rb 100.00 % 80 36 36 0 1.00
rails_test_app/logstruct_test_app/test/integration/lograge_formatter_integration_test.rb 95.24 % 44 21 20 1 1.43
rails_test_app/logstruct_test_app/test/integration/puma_integration_test.rb 97.92 % 101 48 47 1 2.04
rails_test_app/logstruct_test_app/test/integration/test_logging_integration_test.rb 82.35 % 78 34 28 6 1.94
rails_test_app/logstruct_test_app/test/models/user_test.rb 100.00 % 10 4 4 0 1.00
rails_test_app/logstruct_test_app/test/test_helper.rb 53.33 % 69 30 16 14 0.53

lib/log_struct.rb

86.49% lines covered

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

82.61% lines covered

138 relevant lines. 114 lines covered and 24 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require_relative "../configuration"
  4. 2 module LogStruct
  5. 2 module Concerns
  6. # Concern for handling errors according to configured modes
  7. 2 module Configuration
  8. 2 module ClassMethods
  9. 2 extend T::Sig
  10. 2 SERVER_COMMAND_ARGS = T.let(["server", "s"].freeze, T::Array[String])
  11. 2 CONSOLE_COMMAND_ARGS = T.let(["console", "c"].freeze, T::Array[String])
  12. 2 EMPTY_ARGV = T.let([].freeze, T::Array[String])
  13. 2 CI_FALSE_VALUES = T.let(["false", "0", "no"].freeze, T::Array[String])
  14. 4 sig { params(block: T.proc.params(config: LogStruct::Configuration).void).void }
  15. 2 def configure(&block)
  16. 34 yield(config)
  17. end
  18. 4 sig { returns(LogStruct::Configuration) }
  19. 2 def config
  20. 13927 LogStruct::Configuration.instance
  21. end
  22. # (Can't use alias_method since this module is extended into LogStruct)
  23. 3 sig { returns(LogStruct::Configuration) }
  24. 2 def configuration
  25. 58 config
  26. end
  27. # Setter method to replace the configuration (for testing purposes)
  28. 3 sig { params(config: LogStruct::Configuration).void }
  29. 2 def configuration=(config)
  30. 118 LogStruct::Configuration.set_instance(config)
  31. end
  32. 4 sig { returns(T::Boolean) }
  33. 2 def enabled?
  34. 11 config.enabled
  35. end
  36. 4 sig { void }
  37. 2 def set_enabled_from_rails_env!
  38. # Set enabled based on current Rails environment and the LOGSTRUCT_ENABLED env var.
  39. # Precedence:
  40. # 1. Check if LOGSTRUCT_ENABLED env var is defined (not an empty string)
  41. # - Sets enabled=true only when value is "true", "yes", "1", etc.
  42. # - Sets enabled=false when value is any other value
  43. # 2. Otherwise, check if current Rails environment is in enabled_environments
  44. # AND one of: Rails::Server is defined, OR test environment with CI=true
  45. # BUT NOT Rails::Console (to exclude interactive console)
  46. # 3. Otherwise, leave as config.enabled (defaults to true)
  47. # Then check if LOGSTRUCT_ENABLED env var is set
  48. 20 config.enabled = if ENV["LOGSTRUCT_ENABLED"]
  49. 5 %w[true t yes y 1].include?(ENV["LOGSTRUCT_ENABLED"]&.strip&.downcase)
  50. else
  51. 15 is_console = console_process?
  52. 15 is_server = server_process?
  53. 15 ci_build?
  54. 15 in_enabled_env = config.enabled_environments.include?(::Rails.env.to_sym)
  55. 15 in_enabled_env && !is_console && (is_server || ::Rails.env.test?)
  56. end
  57. end
  58. 3 sig { returns(T::Boolean) }
  59. 2 def is_local?
  60. 1 config.local_environments.include?(::Rails.env.to_sym)
  61. end
  62. 3 sig { returns(T::Boolean) }
  63. 2 def is_production?
  64. 1 !is_local?
  65. end
  66. 4 sig { void }
  67. 2 def merge_rails_filter_parameters!
  68. 4 return unless ::Rails.application.config.respond_to?(:filter_parameters)
  69. 4 rails_filter_params = ::Rails.application.config.filter_parameters
  70. 4 return unless rails_filter_params.is_a?(Array)
  71. 4 return if rails_filter_params.empty?
  72. 3 symbol_filters = T.let([], T::Array[Symbol])
  73. 3 matchers = T.let([], T::Array[ConfigStruct::FilterMatcher])
  74. 3 leftovers = T.let([], T::Array[T.untyped])
  75. 3 rails_filter_params.each do |entry|
  76. 12 matcher = build_filter_matcher(entry)
  77. 12 if matcher
  78. 1 matchers << matcher
  79. 1 next
  80. end
  81. 11 normalized_symbol = normalize_filter_symbol(entry)
  82. 11 if normalized_symbol
  83. 11 symbol_filters << normalized_symbol
  84. else
  85. leftovers << entry
  86. end
  87. end
  88. 3 if symbol_filters.any?
  89. 2 config.filters.filter_keys |= symbol_filters
  90. end
  91. 3 if matchers.any?
  92. 1 matchers.each do |matcher|
  93. 1 existing = config.filters.filter_matchers.any? do |registered|
  94. 1 registered.label == matcher.label
  95. end
  96. 1 config.filters.filter_matchers << matcher unless existing
  97. end
  98. end
  99. 3 replace_filter_parameters(rails_filter_params, leftovers)
  100. end
  101. 2 private
  102. 3 sig { returns(T::Boolean) }
  103. 2 def console_process?
  104. 15 return true if defined?(::Rails::Console)
  105. 50 current_argv.any? { |arg| CONSOLE_COMMAND_ARGS.include?(arg) }
  106. end
  107. 3 sig { returns(T::Boolean) }
  108. 2 def server_process?
  109. 15 return true if logstruct_server_mode?
  110. 52 current_argv.any? { |arg| SERVER_COMMAND_ARGS.include?(arg) }
  111. end
  112. 3 sig { returns(T::Boolean) }
  113. 2 def logstruct_server_mode?
  114. 15 ::LogStruct.server_mode?
  115. end
  116. 3 sig { returns(T::Array[String]) }
  117. 2 def current_argv
  118. 28 raw = ::ARGV
  119. 102 strings = raw.map { |arg| arg.to_s }
  120. 28 T.let(strings, T::Array[String])
  121. rescue NameError
  122. EMPTY_ARGV
  123. end
  124. 3 sig { returns(T::Boolean) }
  125. 2 def ci_build?
  126. 15 value = ENV["CI"]
  127. 15 return false if value.nil?
  128. 14 normalized = value.strip.downcase
  129. 14 return false if normalized.empty?
  130. 13 !CI_FALSE_VALUES.include?(normalized)
  131. end
  132. 3 sig { params(filter: T.untyped).returns(T.nilable(Symbol)) }
  133. 2 def normalize_filter_symbol(filter)
  134. 11 return filter if filter.is_a?(Symbol)
  135. 2 return filter.downcase.to_sym if filter.is_a?(String)
  136. return nil unless filter.respond_to?(:to_sym)
  137. begin
  138. sym = filter.to_sym
  139. sym.is_a?(Symbol) ? sym : nil
  140. rescue
  141. nil
  142. end
  143. end
  144. 3 sig { params(filter: T.untyped).returns(T.nilable(ConfigStruct::FilterMatcher)) }
  145. 2 def build_filter_matcher(filter)
  146. 12 case filter
  147. when ::Regexp
  148. 1 callable = Kernel.lambda do |key, _value|
  149. 1 filter.match?(key)
  150. end
  151. 1 return ConfigStruct::FilterMatcher.new(callable: callable, label: filter.inspect)
  152. else
  153. 11 return build_callable_filter_matcher(filter) if callable_filter?(filter)
  154. end
  155. 11 nil
  156. end
  157. 3 sig { params(filter: T.untyped).returns(T::Boolean) }
  158. 2 def callable_filter?(filter)
  159. 11 filter.respond_to?(:call)
  160. end
  161. 2 sig { params(filter: T.untyped).returns(T.nilable(ConfigStruct::FilterMatcher)) }
  162. 2 def build_callable_filter_matcher(filter)
  163. callable = Kernel.lambda do |key, value|
  164. call_args = case arity_for_filter(filter)
  165. when 0
  166. []
  167. when 1
  168. [key]
  169. else
  170. [key, value]
  171. end
  172. result = filter.call(*call_args)
  173. !!result
  174. rescue ArgumentError
  175. begin
  176. !!filter.call(key)
  177. rescue => e
  178. handle_filter_error(e, filter, key)
  179. false
  180. end
  181. rescue => e
  182. handle_filter_error(e, filter, key)
  183. false
  184. end
  185. ConfigStruct::FilterMatcher.new(callable: callable, label: filter.inspect)
  186. end
  187. 2 sig { params(filter: T.untyped).returns(Integer) }
  188. 2 def arity_for_filter(filter)
  189. filter.respond_to?(:arity) ? filter.arity : 2
  190. end
  191. 3 sig { params(filter_params: T::Array[T.untyped], leftovers: T::Array[T.untyped]).void }
  192. 2 def replace_filter_parameters(filter_params, leftovers)
  193. 3 filter_params.clear
  194. 3 filter_params.concat(leftovers)
  195. end
  196. 2 sig { params(error: StandardError, filter: T.untyped, key: String).void }
  197. 2 def handle_filter_error(error, filter, key)
  198. context = {
  199. filter: filter.class.name,
  200. key: key,
  201. filter_label: begin
  202. filter.inspect
  203. rescue
  204. "unknown"
  205. end
  206. }
  207. LogStruct.handle_exception(error, source: Source::Internal, context: context)
  208. end
  209. end
  210. end
  211. end
  212. end

lib/log_struct/concerns/error_handling.rb

86.49% lines covered

37 relevant lines. 32 lines covered and 5 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. 9 config = LogStruct.config
  16. # Use a case statement for type-safety
  17. 9 case source
  18. when Source::TypeChecking
  19. 1 config.error_handling_modes.type_checking_errors
  20. when Source::Internal
  21. config.error_handling_modes.logstruct_errors
  22. when Source::Security
  23. 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. 8 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. 3 error_log = Log.from_exception(source, error, context || {})
  37. 3 LogStruct.error(error_log)
  38. end
  39. # Report an error using the configured handler or MultiErrorReporter
  40. 3 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. 1 log_error(error, source: source, context: context)
  43. 1 error_handler = LogStruct.config.error_reporting_handler
  44. 1 if error_handler
  45. # Use the configured handler
  46. error_handler.call(error, context, source)
  47. else
  48. # Fall back to MultiErrorReporter (detects Sentry, Bugsnag, etc.)
  49. 1 LogStruct::MultiErrorReporter.report_error(error, context || {})
  50. end
  51. end
  52. # Handle an error according to the configured error handling mode (log, report, raise, etc)
  53. 3 sig { params(error: StandardError, source: Source, context: T.nilable(T::Hash[Symbol, T.untyped])).void }
  54. 2 def handle_exception(error, source:, context: nil)
  55. 8 mode = error_handling_mode_for(source)
  56. # Log / report in production, raise locally (dev/test)
  57. 8 if mode == ErrorHandlingMode::LogProduction || mode == ErrorHandlingMode::ReportProduction
  58. 3 raise(error) if !LogStruct.is_production?
  59. end
  60. 6 case mode
  61. when ErrorHandlingMode::Ignore
  62. # Do nothing
  63. when ErrorHandlingMode::Raise
  64. 2 raise(error)
  65. when ErrorHandlingMode::Log, ErrorHandlingMode::LogProduction
  66. 2 log_error(error, source: source, context: context)
  67. when ErrorHandlingMode::Report, ErrorHandlingMode::ReportProduction
  68. 1 log_and_report_error(error, source: source, context: context)
  69. else
  70. # Ensures the case statement is exhaustive
  71. T.absurd(mode)
  72. end
  73. end
  74. end
  75. end
  76. end
  77. 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. 16 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. 5 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. 2 prop :type_checking_errors, ErrorHandlingMode, default: ErrorHandlingMode::LogProduction
  17. 2 prop :logstruct_errors, ErrorHandlingMode, default: ErrorHandlingMode::LogProduction
  18. 2 prop :security_errors, ErrorHandlingMode, default: ErrorHandlingMode::Report
  19. 2 prop :standard_errors, ErrorHandlingMode, default: ErrorHandlingMode::Raise
  20. end
  21. end
  22. end

lib/log_struct/config_struct/filters.rb

100.0% lines covered

26 relevant lines. 26 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 FilterMatcher < T::Struct
  6. 2 extend T::Sig
  7. 2 const :callable, T.proc.params(key: String, value: T.untyped).returns(T::Boolean)
  8. 2 const :label, String
  9. 3 sig { params(key: String, value: T.untyped).returns(T::Boolean) }
  10. 2 def matches?(key, value)
  11. 3036 callable.call(key, value)
  12. end
  13. end
  14. 2 class Filters < T::Struct
  15. 2 include Sorbet::SerializeSymbolKeys
  16. # Keys that should be filtered in nested structures such as request params and job arguments.
  17. # Filtered data includes information about Hashes and Arrays.
  18. #
  19. # { _filtered: {
  20. # _class: "Hash", # Class of the filtered value
  21. # _bytes: 1234, # Length of JSON string in bytes
  22. # _keys_count: 3, # Number of keys in the hash
  23. # _keys: [:key1, :key2, :key3], # First 10 keys in the hash
  24. # }
  25. # }
  26. #
  27. # Default: [:password, :password_confirmation, :pass, :pw, :token, :secret,
  28. # :credentials, :creds, :auth, :authentication, :authorization]
  29. #
  30. 2 prop :filter_keys,
  31. T::Array[Symbol],
  32. factory: -> {
  33. 50 %i[
  34. password password_confirmation pass pw token secret
  35. credentials auth authentication authorization
  36. credit_card ssn social_security
  37. ]
  38. }
  39. # Keys where string values should include an SHA256 hash.
  40. # Useful for tracing emails across requests (e.g. sign in, sign up) while protecting privacy.
  41. # Default: [:email, :email_address]
  42. 2 prop :filter_keys_with_hashes,
  43. T::Array[Symbol],
  44. 50 factory: -> { %i[email email_address] }
  45. # Hash salt for SHA256 hashing (typically used for email addresses)
  46. # Used for both param filters and string scrubbing
  47. # Default: "l0g5t0p"
  48. 2 prop :hash_salt, String, default: "l0g5t0p"
  49. # Hash length for SHA256 hashing (typically used for email addresses)
  50. # Used for both param filters and string scrubbing
  51. # Default: 12
  52. 2 prop :hash_length, Integer, default: 12
  53. # Filter email addresses. Also controls email filtering for the ActionMailer integration
  54. # (to, from, recipient fields, etc.)
  55. # Default: true
  56. 2 prop :email_addresses, T::Boolean, default: true
  57. # Filter URL passwords
  58. # Default: true
  59. 2 prop :url_passwords, T::Boolean, default: true
  60. # Filter credit card numbers
  61. # Default: true
  62. 2 prop :credit_card_numbers, T::Boolean, default: true
  63. # Filter phone numbers
  64. # Default: true
  65. 2 prop :phone_numbers, T::Boolean, default: true
  66. # Filter social security numbers
  67. # Default: true
  68. 2 prop :ssns, T::Boolean, default: true
  69. # Filter IP addresses
  70. # Default: false
  71. 2 prop :ip_addresses, T::Boolean, default: false
  72. # Filter MAC addresses
  73. # Default: false
  74. 2 prop :mac_addresses, T::Boolean, default: false
  75. # Additional filter matchers built from Rails filter_parameters entries that aren't simple symbols.
  76. # Each matcher receives the key (String) and optional value, returning true when the pair should be filtered.
  77. 2 prop :filter_matchers,
  78. T::Array[FilterMatcher],
  79. 50 factory: -> { [] }
  80. end
  81. end
  82. end

lib/log_struct/config_struct/integrations.rb

100.0% lines covered

26 relevant lines. 26 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. 53 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 SemanticLogger integration for high-performance logging
  49. # Default: true
  50. 2 prop :enable_semantic_logger, T::Boolean, default: true
  51. # Enable SQL query logging through ActiveRecord instrumentation
  52. # Default: false (can be resource intensive)
  53. 2 prop :enable_sql_logging, T::Boolean, default: false
  54. # Only log SQL queries slower than this threshold (in milliseconds)
  55. # Set to 0 or nil to log all queries
  56. # Default: 100.0 (log queries taking >100ms)
  57. 2 prop :sql_slow_query_threshold, T.nilable(Float), default: 100.0
  58. # Include bind parameters in SQL logs (disable in production for security)
  59. # Default: true in development/test, false in production
  60. 53 prop :sql_log_bind_params, T::Boolean, factory: -> { !defined?(::Rails) || !::Rails.respond_to?(:env) || !::Rails.env.production? }
  61. # Enable Ahoy (analytics events) integration
  62. # Default: true (safe no-op unless Ahoy is defined)
  63. 2 prop :enable_ahoy, T::Boolean, default: true
  64. # Enable ActiveModelSerializers integration
  65. # Default: true (safe no-op unless ActiveModelSerializers is defined)
  66. 2 prop :enable_active_model_serializers, T::Boolean, default: true
  67. # Enable dotenv-rails integration (convert to structured logs)
  68. # Default: true
  69. 2 prop :enable_dotenv, T::Boolean, default: true
  70. # Enable Puma integration (convert server lifecycle logs)
  71. # Default: true
  72. 2 prop :enable_puma, T::Boolean, default: true
  73. end
  74. end
  75. end

lib/log_struct/configuration.rb

100.0% lines covered

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

30 relevant lines. 30 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. # File storage events (ActiveStorage, Shrine, CarrierWave, etc.)
  17. 2 Upload = new(:upload)
  18. 2 Download = new(:download)
  19. 2 Delete = new(:delete)
  20. 2 Metadata = new(:metadata)
  21. 2 Exist = new(:exist)
  22. 2 Stream = new(:stream)
  23. 2 Url = new(:url)
  24. # Data generation events
  25. 2 Generate = new(:generate)
  26. # Email events
  27. 2 Delivery = new(:delivery)
  28. 2 Delivered = new(:delivered)
  29. # Configuration / boot events
  30. 2 Load = new(:load)
  31. 2 Update = new(:update)
  32. 2 Save = new(:save)
  33. 2 Restore = new(:restore)
  34. # Server lifecycle (e.g., Puma)
  35. # Start already defined above
  36. 2 Shutdown = new(:shutdown)
  37. # Security events
  38. 2 IPSpoof = new(:ip_spoof)
  39. 2 CSRFViolation = new(:csrf_violation)
  40. 2 BlockedHost = new(:blocked_host)
  41. # Database events
  42. 2 Database = new(:database)
  43. # Error events
  44. 2 Error = new(:error)
  45. # Fallback
  46. 2 Unknown = new(:unknown)
  47. end
  48. end
  49. 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. 899 return Unknown if severity.nil?
  33. 898 return from_severity_int(severity) if severity.is_a?(Integer)
  34. 887 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. 887 case severity.to_s.downcase.to_sym
  39. 15 when :debug then Debug
  40. 834 when :info then Info
  41. 8 when :warn then Warn
  42. 21 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. 11 case severity
  50. 1 when ::Logger::DEBUG then Debug
  51. 5 when ::Logger::INFO then Info
  52. 1 when ::Logger::WARN then Warn
  53. 1 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

111 relevant lines. 111 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(:request_id)
  32. # HTTP-specific fields
  33. 2 Format = new(:format)
  34. 2 Controller = new(:controller)
  35. 2 Action = new(:action)
  36. 2 Status = new(:status)
  37. # DurationMs already defined below for general metrics
  38. 2 View = new(:view)
  39. 2 Database = new(:db)
  40. 2 Params = new(:params)
  41. # Security-specific fields
  42. 2 BlockedHost = new(:blocked_host)
  43. 2 BlockedHosts = new(:blocked_hosts)
  44. 2 AllowedHosts = new(:allowed_hosts)
  45. 2 AllowIpHosts = new(:allow_ip_hosts)
  46. 2 ClientIp = new(:client_ip)
  47. 2 XForwardedFor = new(:x_forwarded_for)
  48. # Email-specific fields
  49. 2 To = new(:to)
  50. 2 From = new(:from)
  51. 2 Subject = new(:subject)
  52. 2 MessageId = new(:msg_id)
  53. 2 MailerClass = new(:mailer)
  54. 2 MailerAction = new(:mailer_action)
  55. 2 AttachmentCount = new(:attachments)
  56. # Error fields
  57. 2 ErrorClass = new(:error_class)
  58. 2 Backtrace = new(:backtrace)
  59. # Job-specific fields
  60. 2 JobId = new(:job_id)
  61. 2 JobClass = new(:job_class)
  62. 2 QueueName = new(:queue_name)
  63. 2 Arguments = new(:arguments)
  64. 2 RetryCount = new(:retry_count)
  65. 2 Retries = new(:retries)
  66. 2 Attempt = new(:attempt)
  67. 2 Executions = new(:executions)
  68. 2 ExceptionExecutions = new(:exception_executions)
  69. 2 ProviderJobId = new(:provider_job_id)
  70. 2 ScheduledAt = new(:scheduled_at)
  71. 2 StartedAt = new(:started_at)
  72. 2 FinishedAt = new(:finished_at)
  73. 2 DurationMs = new(:duration_ms)
  74. 2 WaitMs = new(:wait_ms)
  75. # Deprecated: ExecutionTime/WaitTime/RunTime
  76. 2 ExecutionTime = new(:execution_time)
  77. 2 WaitTime = new(:wait_time)
  78. 2 RunTime = new(:run_time)
  79. 2 Priority = new(:priority)
  80. 2 CronKey = new(:cron_key)
  81. 2 ErrorMessage = new(:error_message)
  82. 2 Result = new(:result)
  83. 2 EnqueueCaller = new(:enqueue_caller)
  84. # Dotenv fields
  85. 2 File = new(:file)
  86. 2 Vars = new(:vars)
  87. 2 Snapshot = new(:snapshot)
  88. # Sidekiq-specific fields
  89. 2 ProcessId = new(:pid)
  90. 2 ThreadId = new(:tid)
  91. 2 Context = new(:ctx)
  92. # Storage-specific fields (ActiveStorage)
  93. 2 Checksum = new(:checksum)
  94. 2 Exist = new(:exist)
  95. 2 Url = new(:url)
  96. 2 Prefix = new(:prefix)
  97. 2 Range = new(:range)
  98. # Storage-specific fields (Shrine)
  99. 2 Storage = new(:storage)
  100. 2 Operation = new(:op)
  101. 2 FileId = new(:file_id)
  102. 2 Filename = new(:filename)
  103. 2 MimeType = new(:mime_type)
  104. 2 Size = new(:size)
  105. 2 Metadata = new(:metadata)
  106. 2 Location = new(:location)
  107. 2 UploadOptions = new(:upload_opts)
  108. 2 DownloadOptions = new(:download_opts)
  109. 2 Options = new(:opts)
  110. 2 Uploader = new(:uploader)
  111. # CarrierWave-specific fields
  112. 2 Model = new(:model)
  113. 2 MountPoint = new(:mount_point)
  114. 2 Version = new(:version)
  115. 2 StorePath = new(:store_path)
  116. 2 Extension = new(:ext)
  117. # SQL-specific fields
  118. 2 Sql = new(:sql)
  119. 2 Name = new(:name)
  120. 2 RowCount = new(:row_count)
  121. # Use Adapter for both AMS and SQL adapter name
  122. 2 BindParams = new(:bind_params)
  123. 2 DatabaseName = new(:db_name)
  124. 2 ConnectionPoolSize = new(:pool_size)
  125. 2 ActiveConnections = new(:active_count)
  126. 2 OperationType = new(:op_type)
  127. 2 TableNames = new(:table_names)
  128. # ActiveModelSerializers fields
  129. 2 Serializer = new(:serializer)
  130. 2 Adapter = new(:adapter)
  131. 2 ResourceClass = new(:resource_class)
  132. # Ahoy-specific fields
  133. 2 AhoyEvent = new(:ahoy_event)
  134. 2 Properties = new(:properties)
  135. # Puma / server lifecycle fields
  136. 2 Mode = new(:mode)
  137. 2 PumaVersion = new(:puma_version)
  138. 2 PumaCodename = new(:puma_codename)
  139. 2 RubyVersion = new(:ruby_version)
  140. 2 MinThreads = new(:min_threads)
  141. 2 MaxThreads = new(:max_threads)
  142. 2 Environment = new(:environment)
  143. 2 ListeningAddresses = new(:listening_addresses)
  144. 2 Address = new(:addr)
  145. end
  146. end
  147. 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

92.16% lines covered

102 relevant lines. 94 lines covered and 8 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 "json"
  6. 2 require "globalid"
  7. 2 require_relative "enums/source"
  8. 2 require_relative "enums/event"
  9. 2 require_relative "string_scrubber"
  10. 2 require_relative "log"
  11. 2 require_relative "param_filters"
  12. 2 require_relative "multi_error_reporter"
  13. 2 module LogStruct
  14. 2 class Formatter < ::Logger::Formatter
  15. 2 extend T::Sig
  16. # Add current_tags method to support ActiveSupport::TaggedLogging
  17. 3 sig { returns(T::Array[String]) }
  18. 2 def current_tags
  19. 7 Thread.current[:activesupport_tagged_logging_tags] ||= []
  20. end
  21. # Add tagged method to support ActiveSupport::TaggedLogging
  22. 3 sig { params(tags: T::Array[String], blk: T.proc.params(formatter: Formatter).void).returns(T.untyped) }
  23. 2 def tagged(*tags, &blk)
  24. 1 new_tags = tags.flatten
  25. 1 current_tags.concat(new_tags) if new_tags.any?
  26. 1 yield self
  27. ensure
  28. 1 current_tags.pop(new_tags.size) if new_tags&.any?
  29. end
  30. # Add clear_tags! method to support ActiveSupport::TaggedLogging
  31. 3 sig { void }
  32. 2 def clear_tags!
  33. 1 Thread.current[:activesupport_tagged_logging_tags] = []
  34. end
  35. 2 sig { params(tags: T::Array[String]).returns(T.untyped) }
  36. 2 def push_tags(*tags)
  37. current_tags.concat(tags)
  38. end
  39. 4 sig { params(string: String).returns(String) }
  40. 2 def scrub_string(string)
  41. # Use StringScrubber module to scrub sensitive information from strings
  42. 4413 StringScrubber.scrub(string)
  43. end
  44. 4 sig { params(arg: T.untyped, recursion_depth: Integer).returns(T.untyped) }
  45. 2 def process_values(arg, recursion_depth: 0)
  46. # Prevent infinite recursion in case any args have circular references
  47. # or are too deeply nested. Just return args.
  48. 5525 return arg if recursion_depth > 20
  49. 5523 case arg
  50. when Hash
  51. 916 result = {}
  52. # Process each key-value pair
  53. 916 arg.each do |key, value|
  54. # Check if this key should be filtered at any depth
  55. 4576 result[key] = if ParamFilters.should_filter_key?(key, value)
  56. # Filter the value
  57. 2 {_filtered: ParamFilters.summarize_json_attribute(key, value)}
  58. else
  59. # Process the value normally
  60. 4574 process_values(value, recursion_depth: recursion_depth + 1)
  61. end
  62. end
  63. 916 result
  64. when Array
  65. 26 process_array(arg, recursion_depth: recursion_depth)
  66. when GlobalID::Identification
  67. begin
  68. 5 arg.to_global_id
  69. rescue
  70. begin
  71. 1 case arg
  72. when ActiveRecord::Base
  73. "#{arg.class}(##{arg.id})"
  74. else
  75. # For non-ActiveRecord objects that failed to_global_id, try to get a string representation
  76. # If this also fails, we want to catch it and return the error placeholder
  77. String(T.cast(arg, Object))
  78. end
  79. rescue => e
  80. 1 LogStruct.handle_exception(e, source: Source::Internal)
  81. 1 "[GLOBALID_ERROR]"
  82. end
  83. end
  84. when Source, Event
  85. arg.serialize
  86. when String
  87. 4413 scrub_string(arg)
  88. when Time
  89. arg.iso8601(3)
  90. else
  91. # Any other type (e.g. Symbol, Integer, Float, Boolean etc.)
  92. 163 arg
  93. end
  94. rescue => e
  95. # Report error through LogStruct's framework
  96. context = {
  97. processor_method: "process_values",
  98. value_type: arg.class.name,
  99. recursion_depth: recursion_depth
  100. }
  101. LogStruct.handle_exception(e, source: Source::Internal, context: context)
  102. arg
  103. end
  104. 4 sig { params(log_value: T.untyped, time: Time).returns(T::Hash[Symbol, T.untyped]) }
  105. 2 def log_value_to_hash(log_value, time:)
  106. 883 case log_value
  107. when Log::Interfaces::CommonFields
  108. # Our log classes all implement a custom #serialize method that use symbol keys
  109. 848 log_value.serialize
  110. when T::Struct
  111. # Default T::Struct.serialize methods returns a hash with string keys, so convert them to symbols
  112. 2 log_value.serialize.deep_symbolize_keys
  113. when Hash
  114. # Use hash as is and convert string keys to symbols
  115. 25 log_value.dup.deep_symbolize_keys
  116. else
  117. # Create a Plain log with the message as a string and serialize it with symbol keys
  118. # log_value can be literally anything: Integer, Float, Boolean, NilClass, etc.
  119. 8 log_message = case log_value
  120. # Handle all the basic types without any further processing
  121. when String, Symbol, TrueClass, FalseClass, NilClass, Array, Hash, Time, Numeric
  122. 5 log_value
  123. else
  124. # Handle the serialization of complex objects in a useful way:
  125. #
  126. # 1. For ActiveRecord models: Use as_json which includes attributes
  127. # 2. For objects with custom as_json implementations: Use their implementation
  128. # 3. For basic objects that only have ActiveSupport's as_json: Use to_s
  129. begin
  130. 3 method_owner = log_value.method(:as_json).owner
  131. # If it's ActiveRecord, ActiveModel, or a custom implementation, use as_json
  132. 2 if method_owner.to_s.include?("ActiveRecord") ||
  133. method_owner.to_s.include?("ActiveModel") ||
  134. method_owner.to_s.exclude?("ActiveSupport::CoreExtensions") &&
  135. method_owner.to_s.exclude?("Object")
  136. 1 log_value.as_json
  137. else
  138. # For plain objects with only the default ActiveSupport as_json
  139. 1 log_value.to_s
  140. end
  141. rescue => e
  142. # Handle serialization errors
  143. context = {
  144. 1 object_class: log_value.class.name,
  145. object_inspect: log_value.inspect.truncate(100)
  146. }
  147. 1 LogStruct.handle_exception(e, source: Source::Internal, context: context)
  148. # Fall back to the string representation to ensure we continue processing
  149. 1 log_value.to_s
  150. end
  151. end
  152. 8 Log::Plain.new(
  153. message: log_message,
  154. timestamp: time
  155. ).serialize
  156. end
  157. end
  158. # Serializes Log (or string) into JSON
  159. 4 sig { params(severity: T.any(String, Symbol, Integer), time: Time, progname: T.nilable(String), log_value: T.untyped).returns(String) }
  160. 2 def call(severity, time, progname, log_value)
  161. 876 level_enum = Level.from_severity(severity)
  162. 876 data = log_value_to_hash(log_value, time: time)
  163. # Filter params, scrub sensitive values, format ActiveJob GlobalID arguments
  164. 876 data = process_values(data)
  165. # Add standard fields if not already present
  166. 876 data[:src] ||= Source::App
  167. 876 data[:evt] ||= Event::Log
  168. 876 data[:ts] ||= time.iso8601(3)
  169. 876 data[:lvl] = level_enum # Set level from severity parameter
  170. 876 data[:prog] = progname if progname.present?
  171. 876 generate_json(data)
  172. end
  173. # Output as JSON with a newline. We mock this method in tests so we can
  174. # inspect the data right before it gets turned into a JSON string.
  175. 4 sig { params(data: T::Hash[T.untyped, T.untyped]).returns(String) }
  176. 2 def generate_json(data)
  177. 877 "#{data.to_json}\n"
  178. end
  179. 4 sig { params(array: T::Array[T.untyped], recursion_depth: Integer).returns(T::Array[T.untyped]) }
  180. 2 def process_array(array, recursion_depth:)
  181. 26 return [] if array.empty?
  182. 24 if looks_like_backtrace_array?(array)
  183. 20 array.map { |value| process_values(value, recursion_depth: recursion_depth + 1) }
  184. else
  185. 20 processed = []
  186. 20 array.each_with_index do |value, index|
  187. 54 break if index >= 10
  188. 52 processed << process_values(value, recursion_depth: recursion_depth + 1)
  189. end
  190. 20 if array.size > 10
  191. 2 processed << "... and #{array.size - 10} more items"
  192. end
  193. 20 processed
  194. end
  195. end
  196. # Check if an array looks like a backtrace (array of strings with file:line pattern)
  197. 3 sig { params(array: T::Array[T.untyped]).returns(T::Boolean) }
  198. 2 def looks_like_backtrace_array?(array)
  199. 24 backtrace_like_count = array.first(5).count do |element|
  200. 58 element.is_a?(String) && element.match?(/\A[^:\s]+:\d+/)
  201. end
  202. 24 backtrace_like_count >= 3
  203. end
  204. end
  205. 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

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 "digest"
  4. 2 module LogStruct
  5. # Utility module for hashing sensitive data
  6. 2 module HashUtils
  7. 2 class << self
  8. 2 extend T::Sig
  9. # Create a hash of a string value for tracing while preserving privacy
  10. 3 sig { params(value: String).returns(String) }
  11. 2 def hash_value(value)
  12. 16 salt = LogStruct.config.filters.hash_salt
  13. 16 length = LogStruct.config.filters.hash_length
  14. 16 Digest::SHA256.hexdigest("#{salt}#{value}")[0...length] || "error"
  15. end
  16. end
  17. end
  18. end

lib/log_struct/integrations.rb

94.74% lines covered

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

lib/log_struct/integrations/action_mailer.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 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 "logger"
  10. 2 require_relative "action_mailer/metadata_collection"
  11. 2 require_relative "action_mailer/event_logging"
  12. 2 require_relative "action_mailer/error_handling"
  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. 9 return nil unless defined?(::ActionMailer)
  24. 9 return nil unless config.enabled
  25. 9 return nil unless config.integrations.enable_actionmailer
  26. # Silence default ActionMailer logs (we use our own structured logging)
  27. # This is required because we replace the logging using our own callbacks
  28. 8 if defined?(::ActionMailer::Base)
  29. 8 ::ActionMailer::Base.logger = ::Logger.new(File::NULL)
  30. end
  31. # Register our custom observers and handlers
  32. # Registering these at the class level means all mailers will use them
  33. 8 ActiveSupport.on_load(:action_mailer) do
  34. 8 prepend LogStruct::Integrations::ActionMailer::EventLogging
  35. 8 prepend LogStruct::Integrations::ActionMailer::ErrorHandling
  36. 8 prepend LogStruct::Integrations::ActionMailer::MetadataCollection
  37. end
  38. # If ActionMailer::Base is already loaded, the on_load hooks won't run
  39. # So we need to apply the modules directly
  40. 8 if defined?(::ActionMailer::Base)
  41. 8 ::ActionMailer::Base.prepend(LogStruct::Integrations::ActionMailer::EventLogging)
  42. 8 ::ActionMailer::Base.prepend(LogStruct::Integrations::ActionMailer::ErrorHandling)
  43. 8 ::ActionMailer::Base.prepend(LogStruct::Integrations::ActionMailer::MetadataCollection)
  44. end
  45. 8 true
  46. end
  47. end
  48. end
  49. end

lib/log_struct/integrations/action_mailer/error_handling.rb

84.85% lines covered

99 relevant lines. 84 lines covered and 15 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. 3 sig { returns(T.nilable(T::Boolean)) }
  17. 2 attr_accessor :logstruct_mail_failed
  18. # NOTE: rescue_from handlers are checked in reverse order of declaration.
  19. # We want LogStruct handlers to be checked AFTER user handlers (lower priority),
  20. # so we need to add them BEFORE user handlers are declared.
  21. # This will be called when the module is included/prepended
  22. 4 sig { params(base: T.untyped).void }
  23. 2 def self.install_handler(base)
  24. # Only add the handler once per class
  25. 16 return if base.instance_variable_get(:@_logstruct_handler_installed)
  26. # Add our handler FIRST so it has lower priority than user handlers
  27. 2 base.rescue_from StandardError, with: :log_and_reraise_error
  28. # Mark as installed to prevent duplicates
  29. 2 base.instance_variable_set(:@_logstruct_handler_installed, true)
  30. end
  31. 2 included do
  32. LogStruct::Integrations::ActionMailer::ErrorHandling.install_handler(self)
  33. end
  34. # Also support prepended (used by tests and manual setup)
  35. 4 sig { params(base: T.untyped).void }
  36. 2 def self.prepended(base)
  37. 16 install_handler(base)
  38. end
  39. 2 protected
  40. # Just log the error without reporting or retrying
  41. 3 sig { params(ex: StandardError).void }
  42. 2 def log_and_ignore_error(ex)
  43. 1 self.logstruct_mail_failed = true
  44. 1 log_email_delivery_error(ex, notify: false, report: false, reraise: false)
  45. end
  46. # Log and report to error service, but doesn't reraise.
  47. 2 sig { params(ex: StandardError).void }
  48. 2 def log_and_report_error(ex)
  49. log_email_delivery_error(ex, notify: false, report: true, reraise: false)
  50. end
  51. # Log, report to error service, and reraise for retry
  52. 3 sig { params(ex: StandardError).void }
  53. 2 def log_and_reraise_error(ex)
  54. 1 log_email_delivery_error(ex, notify: false, report: true, reraise: true)
  55. end
  56. 2 private
  57. # Handle an error from a mailer
  58. 3 sig { params(mailer: T.untyped, error: StandardError, message: String).void }
  59. 2 def log_structured_error(mailer, error, message)
  60. # Get message if available
  61. 2 mailer_message = mailer.respond_to?(:message) ? mailer.message : nil
  62. # Prepare universal mailer fields
  63. 2 message_data = {}
  64. 2 MetadataCollection.add_message_metadata(mailer, message_data)
  65. # Prepare app-specific context data for additional_data
  66. 2 context_data = {}
  67. 2 MetadataCollection.add_context_metadata(mailer, context_data)
  68. # Extract email fields
  69. 2 to = mailer_message&.to
  70. 2 from = mailer_message&.from&.first
  71. 2 subject = mailer_message&.subject
  72. 2 message_id = extract_message_id_from_mailer(mailer)
  73. # Create ActionMailer-specific error struct
  74. 2 exception_data = Log::ActionMailer::Error.new(
  75. to: to,
  76. from: from,
  77. subject: subject,
  78. message_id: message_id,
  79. mailer_class: mailer.class.to_s,
  80. 2 mailer_action: mailer.respond_to?(:action_name) ? mailer.action_name&.to_s : nil,
  81. attachment_count: message_data[:attachment_count],
  82. error_class: error.class,
  83. message: message,
  84. backtrace: error.backtrace,
  85. additional_data: context_data.presence,
  86. timestamp: Time.now
  87. )
  88. # Log the structured error
  89. 2 LogStruct.error(exception_data)
  90. end
  91. # Extract message ID from the mailer
  92. 3 sig { params(mailer: T.untyped).returns(T.nilable(String)) }
  93. 2 def extract_message_id_from_mailer(mailer)
  94. 3 return nil unless mailer.respond_to?(:message)
  95. 3 mail_message = mailer.message
  96. 3 return nil unless mail_message.respond_to?(:message_id)
  97. 3 mail_message.message_id
  98. end
  99. # Log when email delivery fails
  100. 3 sig { params(error: StandardError, notify: T::Boolean, report: T::Boolean, reraise: T::Boolean).void }
  101. 2 def log_email_delivery_error(error, notify: false, report: true, reraise: true)
  102. # Generate appropriate error message
  103. 2 message = error_message_for(error, reraise)
  104. # Use structured error logging
  105. 2 log_structured_error(self, error, message)
  106. # Handle notifications and reporting
  107. 2 handle_error_notifications(error, notify, report, reraise)
  108. end
  109. # Generate appropriate error message based on error handling strategy
  110. 3 sig { params(error: StandardError, reraise: T::Boolean).returns(String) }
  111. 2 def error_message_for(error, reraise)
  112. 2 if reraise
  113. 1 "#{error.class}: Email delivery error, will retry. Recipients: #{recipients(error)}. Error message: #{error.message}"
  114. else
  115. 1 "#{error.class}: Cannot send email to #{recipients(error)}. Error message: #{error.message}"
  116. end
  117. end
  118. # Handle error notifications, reporting, and reraising
  119. 3 sig { params(error: StandardError, notify: T::Boolean, report: T::Boolean, reraise: T::Boolean).void }
  120. 2 def handle_error_notifications(error, notify, report, reraise)
  121. # Log a notification event if requested
  122. 2 log_notification_event(error) if notify
  123. # Report to error reporting service if requested
  124. 2 if report
  125. # Get message if available
  126. 1 mailer_message = respond_to?(:message) ? message : nil
  127. # Prepare universal mailer fields
  128. 1 message_data = {}
  129. 1 MetadataCollection.add_message_metadata(self, message_data)
  130. # Prepare app-specific context data
  131. 1 context_data = {recipients: recipients(error)}
  132. 1 MetadataCollection.add_context_metadata(self, context_data)
  133. # Extract email fields
  134. 1 to = mailer_message&.to
  135. 1 from = mailer_message&.from&.first
  136. 1 subject = mailer_message&.subject
  137. 1 message_id = extract_message_id_from_mailer(self)
  138. # Create ActionMailer-specific error struct
  139. 1 exception_data = Log::ActionMailer::Error.new(
  140. to: to,
  141. from: from,
  142. subject: subject,
  143. message_id: message_id,
  144. mailer_class: self.class.to_s,
  145. 1 mailer_action: respond_to?(:action_name) ? action_name&.to_s : nil,
  146. attachment_count: message_data[:attachment_count],
  147. error_class: error.class,
  148. message: error.message,
  149. backtrace: error.backtrace,
  150. additional_data: context_data.presence,
  151. timestamp: Time.now
  152. )
  153. # Log the exception with structured data
  154. 1 LogStruct.error(exception_data)
  155. # Call the error handler with flat context for compatibility
  156. context = {
  157. 1 mailer_class: self.class.to_s,
  158. 1 mailer_action: respond_to?(:action_name) ? action_name : nil,
  159. recipients: recipients(error)
  160. }
  161. 1 LogStruct.handle_exception(error, source: Source::Mailer, context: context)
  162. end
  163. # Re-raise the error if requested
  164. 1 Kernel.raise error if reraise
  165. end
  166. # Log a notification event that can be picked up by external systems
  167. 2 sig { params(error: StandardError).void }
  168. 2 def log_notification_event(error)
  169. # Get message if available
  170. mailer_message = respond_to?(:message) ? message : nil
  171. # Prepare universal mailer fields
  172. message_data = {}
  173. MetadataCollection.add_message_metadata(self, message_data)
  174. # Prepare app-specific context data
  175. context_data = {
  176. mailer: self.class.to_s,
  177. action: action_name&.to_s,
  178. recipients: recipients(error)
  179. }
  180. MetadataCollection.add_context_metadata(self, context_data)
  181. # Extract email fields
  182. to = mailer_message&.to
  183. from = mailer_message&.from&.first
  184. subject = mailer_message&.subject
  185. message_id = extract_message_id_from_mailer(self)
  186. # Create ActionMailer-specific error struct
  187. exception_data = Log::ActionMailer::Error.new(
  188. to: to,
  189. from: from,
  190. subject: subject,
  191. message_id: message_id,
  192. mailer_class: self.class.to_s,
  193. mailer_action: respond_to?(:action_name) ? action_name&.to_s : nil,
  194. attachment_count: message_data[:attachment_count],
  195. error_class: error.class,
  196. message: error.message,
  197. backtrace: error.backtrace,
  198. additional_data: context_data.presence,
  199. timestamp: Time.now,
  200. level: Level::Info
  201. )
  202. # Log the error at info level since it's not a critical error
  203. LogStruct.info(exception_data)
  204. end
  205. 3 sig { params(error: StandardError).returns(String) }
  206. 2 def recipients(error)
  207. # Extract recipient info if available
  208. 4 if error.respond_to?(:recipients) && T.unsafe(error).recipients.present?
  209. T.unsafe(error).recipients.join(", ")
  210. else
  211. 4 "unknown"
  212. end
  213. end
  214. end
  215. end
  216. end
  217. 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. 5 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. 5 return if logstruct_mail_failed
  39. 4 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. 9 mailer_message = message if respond_to?(:message)
  47. # Prepare universal mailer fields
  48. 9 message_data = {}
  49. 9 MetadataCollection.add_message_metadata(self, message_data)
  50. # Prepare app-specific context data for additional_data
  51. 9 context_data = {}
  52. 9 MetadataCollection.add_context_metadata(self, context_data)
  53. 9 context_data.merge!(additional_data) if additional_data.present?
  54. # Extract email fields (these will be filtered if email_addresses=true)
  55. 9 to = mailer_message&.to
  56. 9 from = mailer_message&.from&.first
  57. 9 subject = mailer_message&.subject
  58. 9 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. 9 log = case event_type
  68. when Event::Delivery
  69. 5 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. 4 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. 9 LogStruct.info(log)
  84. 9 log
  85. end
  86. # Extract message ID from the mailer
  87. 3 sig { returns(T.nilable(String)) }
  88. 2 def extract_message_id
  89. 9 return nil unless respond_to?(:message)
  90. 9 mail_message = message
  91. 9 return nil unless mail_message.respond_to?(:message_id)
  92. 9 mail_message.message_id
  93. end
  94. end
  95. end
  96. end
  97. end

lib/log_struct/integrations/action_mailer/metadata_collection.rb

90.91% lines covered

33 relevant lines. 30 lines covered and 3 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. 14 message = mailer.respond_to?(:message) ? mailer.message : nil
  13. # Add attachment count if message is available
  14. 14 log_data[:attachment_count] = if message
  15. 13 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. 14 extract_ids_to_log_data(mailer, log_data)
  25. # Add any current tags from ActiveJob or ActionMailer
  26. 14 add_current_tags_to_log_data(log_data)
  27. end
  28. 3 sig { params(mailer: T.untyped, log_data: T::Hash[Symbol, T.untyped]).void }
  29. 2 def self.extract_ids_to_log_data(mailer, log_data)
  30. # Use configured ID mapping from LogStruct configuration
  31. 14 id_mapping = LogStruct.config.integrations.actionmailer_id_mapping
  32. 14 id_mapping.each do |ivar_name, log_key|
  33. 28 ivar = :"@#{ivar_name}"
  34. 28 next unless mailer.instance_variable_defined?(ivar)
  35. obj = mailer.instance_variable_get(ivar)
  36. log_data[log_key] = obj.id if obj.respond_to?(:id)
  37. end
  38. end
  39. 3 sig { params(log_data: T::Hash[Symbol, T.untyped]).void }
  40. 2 def self.add_current_tags_to_log_data(log_data)
  41. # Get current tags from thread-local storage or ActiveSupport::TaggedLogging
  42. 14 tags = if ::ActiveSupport::TaggedLogging.respond_to?(:current_tags)
  43. 14 T.unsafe(::ActiveSupport::TaggedLogging).current_tags
  44. else
  45. Thread.current[:activesupport_tagged_logging_tags] || []
  46. end
  47. 14 log_data[:tags] = tags if tags.present?
  48. # Get request_id from ActionDispatch if available
  49. 14 if ::ActionDispatch::Request.respond_to?(:current_request_id) &&
  50. T.unsafe(::ActionDispatch::Request).current_request_id.present?
  51. 4 log_data[:request_id] = T.unsafe(::ActionDispatch::Request).current_request_id
  52. end
  53. # Get job_id from ActiveJob if available
  54. 14 if defined?(::ActiveJob::Logging) && ::ActiveJob::Logging.respond_to?(:job_id) &&
  55. T.unsafe(::ActiveJob::Logging).job_id.present?
  56. 3 log_data[:job_id] = T.unsafe(::ActiveJob::Logging).job_id
  57. end
  58. end
  59. end
  60. end
  61. end
  62. 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. 2 return nil unless defined?(::ActiveJob::LogSubscriber)
  20. 2 return nil unless config.enabled
  21. 2 return nil unless config.integrations.enable_activejob
  22. 2 ::ActiveSupport.on_load(:active_job) do
  23. # Detach the default text formatter
  24. 2 ::ActiveJob::LogSubscriber.detach_from :active_job
  25. # Attach our structured formatter
  26. 2 Integrations::ActiveJob::LogSubscriber.attach_to :active_job
  27. end
  28. 2 true
  29. end
  30. end
  31. end
  32. end

lib/log_struct/integrations/active_job/log_subscriber.rb

43.64% lines covered

55 relevant lines. 24 lines covered and 31 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_job"
  6. 2 require_relative "../../log/error"
  7. 2 module LogStruct
  8. 2 module Integrations
  9. 2 module ActiveJob
  10. # Structured logging for ActiveJob
  11. 2 class LogSubscriber < ::ActiveJob::LogSubscriber
  12. 2 extend T::Sig
  13. 2 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  14. 2 def enqueue(event)
  15. job = T.cast(event.payload[:job], ::ActiveJob::Base)
  16. ts = event.time ? Time.at(event.time) : Time.now
  17. base_fields = build_base_fields(job)
  18. logger.info(Log::ActiveJob::Enqueue.new(
  19. **base_fields.to_kwargs,
  20. timestamp: ts
  21. ))
  22. end
  23. 2 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  24. 2 def enqueue_at(event)
  25. job = T.cast(event.payload[:job], ::ActiveJob::Base)
  26. ts = event.time ? Time.at(event.time) : Time.now
  27. base_fields = build_base_fields(job)
  28. logger.info(Log::ActiveJob::Schedule.new(
  29. **base_fields.to_kwargs,
  30. scheduled_at: job.scheduled_at,
  31. timestamp: ts
  32. ))
  33. end
  34. 2 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  35. 2 def perform(event)
  36. job = T.cast(event.payload[:job], ::ActiveJob::Base)
  37. exception = event.payload[:exception_object]
  38. if exception
  39. # Log the exception with the job context
  40. log_exception(exception, job, event)
  41. else
  42. start_float = event.time
  43. end_float = event.end
  44. ts = start_float ? Time.at(start_float) : Time.now
  45. finished_at = end_float ? Time.at(end_float) : Time.now
  46. base_fields = build_base_fields(job)
  47. logger.info(Log::ActiveJob::Finish.new(
  48. **base_fields.to_kwargs,
  49. duration_ms: event.duration.to_f,
  50. finished_at: finished_at,
  51. timestamp: ts
  52. ))
  53. end
  54. end
  55. 2 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  56. 2 def perform_start(event)
  57. job = T.cast(event.payload[:job], ::ActiveJob::Base)
  58. ts = event.time ? Time.at(event.time) : Time.now
  59. started_at = ts
  60. attempt = job.executions
  61. base_fields = build_base_fields(job)
  62. logger.info(Log::ActiveJob::Start.new(
  63. **base_fields.to_kwargs,
  64. started_at: started_at,
  65. attempt: attempt,
  66. timestamp: ts
  67. ))
  68. end
  69. 2 private
  70. 2 sig { params(job: ::ActiveJob::Base).returns(Log::ActiveJob::BaseFields) }
  71. 2 def build_base_fields(job)
  72. Log::ActiveJob::BaseFields.new(
  73. job_id: job.job_id,
  74. job_class: job.class.to_s,
  75. queue_name: job.queue_name&.to_sym,
  76. executions: job.executions,
  77. provider_job_id: job.provider_job_id,
  78. arguments: ((job.class.respond_to?(:log_arguments?) && job.class.log_arguments?) ? job.arguments : nil)
  79. )
  80. end
  81. 2 sig { params(exception: StandardError, job: ::ActiveJob::Base, _event: ::ActiveSupport::Notifications::Event).void }
  82. 2 def log_exception(exception, job, _event)
  83. base_fields = build_base_fields(job)
  84. job_context = base_fields.to_kwargs
  85. log_data = Log.from_exception(Source::Job, exception, job_context)
  86. logger.error(log_data)
  87. end
  88. 2 sig { returns(T.untyped) }
  89. 2 def logger
  90. ::ActiveJob::Base.logger
  91. end
  92. end
  93. end
  94. end
  95. end

lib/log_struct/integrations/active_model_serializers.rb

94.44% lines covered

18 relevant lines. 17 lines covered and 1 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. 3 return nil unless defined?(::ActiveSupport::Notifications)
  13. # Only activate if AMS appears to be present
  14. 3 return nil unless defined?(::ActiveModelSerializers)
  15. # Subscribe to common AMS notification names; keep broad but specific
  16. 1 pattern = /\.active_model_serializers\z/
  17. 1 ::ActiveSupport::Notifications.subscribe(pattern) do |_name, started, finished, _unique_id, payload|
  18. # started/finished are Time; convert to ms
  19. 1 duration_ms = ((finished - started) * 1000.0).round(3)
  20. 1 serializer = payload[:serializer] || payload[:serializer_class]
  21. 1 adapter = payload[:adapter]
  22. 1 resource = payload[:resource] || payload[:object]
  23. 1 LogStruct.info(
  24. LogStruct::Log::ActiveModelSerializers.new(
  25. message: "ams.render",
  26. serializer: serializer&.to_s,
  27. adapter: adapter&.to_s,
  28. resource_class: resource&.class&.name,
  29. duration_ms: duration_ms,
  30. timestamp: started
  31. )
  32. )
  33. rescue => e
  34. LogStruct.handle_exception(e, source: LogStruct::Source::Rails, context: {integration: :active_model_serializers})
  35. end
  36. 1 true
  37. end
  38. end
  39. end
  40. end

lib/log_struct/integrations/active_record.rb

92.68% lines covered

123 relevant lines. 114 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. # - SQL queries are always parameterized (safe)
  26. # - Bind parameters filtered through LogStruct's param filters
  27. # - Sensitive patterns automatically scrubbed
  28. #
  29. # ## Configuration:
  30. # ```ruby
  31. # LogStruct.configure do |config|
  32. # config.integrations.enable_sql_logging = true
  33. # config.integrations.sql_slow_query_threshold = 100.0 # ms
  34. # config.integrations.sql_log_bind_params = false # disable in production
  35. # end
  36. # ```
  37. 2 module ActiveRecord
  38. 2 extend T::Sig
  39. 2 extend IntegrationInterface
  40. # Track subscription state keyed to the current Notifications.notifier instance
  41. 2 State = ::Struct.new(:subscribed, :notifier_id)
  42. 2 STATE = T.let(State.new(false, nil), State)
  43. # Set up SQL query logging integration
  44. 3 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  45. 2 def self.setup(config)
  46. 16 return nil unless config.integrations.enable_sql_logging
  47. 15 return nil unless defined?(::ActiveRecord::Base)
  48. # Detach Rails' default ActiveRecord log subscriber to prevent
  49. # duplicate/unstructured SQL debug output when LogStruct SQL logging
  50. # is enabled. We still receive notifications via ActiveSupport.
  51. 14 if defined?(::ActiveRecord::LogSubscriber)
  52. begin
  53. ::ActiveRecord::LogSubscriber.detach_from(:active_record)
  54. rescue => e
  55. LogStruct.handle_exception(e, source: LogStruct::Source::Internal)
  56. end
  57. end
  58. # Disable verbose query logs ("↳ caller") since LogStruct provides
  59. # structured context and these lines are noisy/unstructured.
  60. 14 if ::ActiveRecord::Base.respond_to?(:verbose_query_logs=)
  61. T.unsafe(::ActiveRecord::Base).verbose_query_logs = false
  62. end
  63. 14 subscribe_to_sql_notifications
  64. 14 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. 14 notifier = ::ActiveSupport::Notifications.notifier
  72. 14 current_id = notifier&.object_id
  73. 14 if STATE.subscribed && STATE.notifier_id == current_id
  74. return
  75. end
  76. 14 ::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. 14 STATE.subscribed = true
  82. 14 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. 31 return if skip_query?(payload)
  89. 24 duration_ms = ((finish - start) * 1000.0).round(2)
  90. # Skip fast queries if threshold is configured
  91. 24 config = LogStruct.config
  92. 24 if config.integrations.sql_slow_query_threshold&.positive?
  93. 2 return if duration_ms < config.integrations.sql_slow_query_threshold
  94. end
  95. 23 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. 22 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. 31 query_name = payload[:name]
  117. 31 sql = payload[:sql]
  118. # Skip Rails schema queries
  119. 31 return true if query_name&.include?("SCHEMA")
  120. 30 return true if query_name&.include?("CACHE")
  121. # Skip common Rails internal queries
  122. 29 return true if sql&.include?("schema_migrations")
  123. 28 return true if sql&.include?("ar_internal_metadata")
  124. # Skip SHOW/DESCRIBE queries
  125. 27 return true if sql&.match?(/\A\s*(SHOW|DESCRIBE|EXPLAIN)\s/i)
  126. 24 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. 23 operation_name = payload[:name] || "SQL Query"
  132. 23 "#{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. 23 row_count = payload[:row_count]
  138. 23 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. 23 connection = payload[:connection]
  144. 23 return nil unless connection
  145. 22 adapter_name = connection.class.name
  146. 22 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. 23 return nil unless LogStruct.config.integrations.sql_log_bind_params
  152. # Prefer type_casted_binds as they're more readable
  153. 22 binds = payload[:type_casted_binds] || payload[:binds]
  154. 22 return nil unless binds
  155. # Filter sensitive data from bind parameters
  156. 2 binds.map do |bind|
  157. 4 filter_bind_parameter(bind)
  158. end
  159. end
  160. # Extract database name from connection
  161. 3 sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
  162. 2 def self.extract_database_name(payload)
  163. 23 connection = payload[:connection]
  164. 23 return nil unless connection
  165. 22 if connection.respond_to?(:current_database)
  166. 22 connection.current_database
  167. elsif connection.respond_to?(:database)
  168. connection.database
  169. end
  170. rescue
  171. nil
  172. end
  173. # Extract connection pool size
  174. 3 sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(Integer)) }
  175. 2 def self.extract_pool_size(payload)
  176. 23 connection = payload[:connection]
  177. 23 return nil unless connection
  178. 22 pool = connection.pool if connection.respond_to?(:pool)
  179. 22 pool&.size
  180. rescue
  181. nil
  182. end
  183. # Extract active connection count
  184. 3 sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(Integer)) }
  185. 2 def self.extract_active_connections(payload)
  186. 23 connection = payload[:connection]
  187. 23 return nil unless connection
  188. 22 pool = connection.pool if connection.respond_to?(:pool)
  189. 22 pool&.stat&.[](:busy)
  190. rescue
  191. nil
  192. end
  193. # Extract SQL operation type (SELECT, INSERT, etc.)
  194. 3 sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
  195. 2 def self.extract_operation_type(payload)
  196. 23 sql = payload[:sql]
  197. 23 return nil unless sql
  198. # Extract first word of SQL query
  199. 23 match = sql.strip.match(/\A\s*(\w+)/i)
  200. 23 match&.captures&.first&.upcase
  201. end
  202. # Extract table names from SQL query
  203. 3 sig { params(payload: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Array[String])) }
  204. 2 def self.extract_table_names(payload)
  205. 23 sql = payload[:sql]
  206. 23 return nil unless sql
  207. # Simple regex to extract table names (basic implementation)
  208. # This covers most common cases but could be enhanced
  209. 23 tables = []
  210. # Match FROM, JOIN, UPDATE, INSERT INTO, DELETE FROM patterns
  211. 23 sql.scan(/(?:FROM|JOIN|UPDATE|INTO|DELETE\s+FROM)\s+["`]?(\w+)["`]?/i) do |match|
  212. 23 table_name = match[0]
  213. 23 tables << table_name unless tables.include?(table_name)
  214. end
  215. 23 tables.empty? ? nil : tables
  216. end
  217. # Filter individual bind parameter values to remove sensitive data
  218. 3 sig { params(value: T.untyped).returns(T.untyped) }
  219. 2 def self.filter_bind_parameter(value)
  220. 4 case value
  221. when String
  222. # Filter strings that look like passwords, tokens, secrets, etc.
  223. 2 if looks_sensitive?(value)
  224. 1 "[FILTERED]"
  225. else
  226. 1 value
  227. end
  228. else
  229. 2 value
  230. end
  231. end
  232. # Check if a string value looks sensitive and should be filtered
  233. 3 sig { params(value: String).returns(T::Boolean) }
  234. 2 def self.looks_sensitive?(value)
  235. # Filter very long strings that might be tokens
  236. 2 return true if value.length > 50
  237. # Filter strings that look like hashed passwords, API keys, tokens
  238. 2 return true if value.match?(/\A[a-f0-9]{32,}\z/i) # MD5, SHA, etc.
  239. 2 return true if value.match?(/\A[A-Za-z0-9+\/]{20,}={0,2}\z/) # Base64
  240. 2 return true if value.match?(/(password|secret|token|key|auth)/i)
  241. 1 false
  242. end
  243. end
  244. end
  245. end

lib/log_struct/integrations/active_storage.rb

39.13% lines covered

46 relevant lines. 18 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. # Subscribe to all ActiveStorage service events
  19. 1 ::ActiveSupport::Notifications.subscribe(/service_.*\.active_storage/) do |*args|
  20. process_active_storage_event(::ActiveSupport::Notifications::Event.new(*args), config)
  21. end
  22. 1 true
  23. end
  24. 2 private_class_method
  25. # Process ActiveStorage events and create structured logs
  26. 2 sig { params(event: ActiveSupport::Notifications::Event, config: LogStruct::Configuration).void }
  27. 2 def self.process_active_storage_event(event, config)
  28. return unless config.enabled
  29. return unless config.integrations.enable_activestorage
  30. # Extract key information from the event
  31. event_name = event.name.sub(/\.active_storage$/, "")
  32. service_name = event.payload[:service]
  33. duration_ms = event.duration
  34. # Map service events to log event types
  35. event_type = case event_name
  36. when "service_upload"
  37. Event::Upload
  38. when "service_download"
  39. Event::Download
  40. when "service_delete"
  41. Event::Delete
  42. when "service_delete_prefixed"
  43. Event::Delete
  44. when "service_exist"
  45. Event::Exist
  46. when "service_url"
  47. Event::Url
  48. when "service_download_chunk"
  49. Event::Download
  50. when "service_stream"
  51. Event::Stream
  52. when "service_update_metadata"
  53. Event::Metadata
  54. else
  55. Event::Unknown
  56. end
  57. # Map the event name to an operation
  58. event_name.sub(/^service_/, "").to_sym
  59. # Create structured log event using generated classes
  60. log_data = case event_type
  61. when Event::Upload
  62. Log::ActiveStorage::Upload.new(
  63. storage: service_name.to_sym,
  64. file_id: event.payload[:key]&.to_s,
  65. checksum: event.payload[:checksum]&.to_s,
  66. duration_ms: duration_ms,
  67. metadata: event.payload[:metadata],
  68. filename: event.payload[:filename],
  69. mime_type: event.payload[:content_type],
  70. size: event.payload[:byte_size]
  71. )
  72. when Event::Download
  73. Log::ActiveStorage::Download.new(
  74. storage: service_name.to_sym,
  75. file_id: event.payload[:key]&.to_s,
  76. filename: event.payload[:filename],
  77. range: event.payload[:range],
  78. duration_ms: duration_ms
  79. )
  80. when Event::Delete
  81. Log::ActiveStorage::Delete.new(
  82. storage: service_name.to_sym,
  83. file_id: event.payload[:key]&.to_s
  84. )
  85. when Event::Metadata
  86. Log::ActiveStorage::Metadata.new(
  87. storage: service_name.to_sym,
  88. file_id: event.payload[:key]&.to_s,
  89. metadata: event.payload[:metadata]
  90. )
  91. when Event::Exist
  92. Log::ActiveStorage::Exist.new(
  93. storage: service_name.to_sym,
  94. file_id: event.payload[:key]&.to_s,
  95. exist: event.payload[:exist]
  96. )
  97. when Event::Stream
  98. Log::ActiveStorage::Stream.new(
  99. storage: service_name.to_sym,
  100. file_id: event.payload[:key]&.to_s,
  101. prefix: event.payload[:prefix]
  102. )
  103. when Event::Url
  104. Log::ActiveStorage::Url.new(
  105. storage: service_name.to_sym,
  106. file_id: event.payload[:key]&.to_s,
  107. url: event.payload[:url]
  108. )
  109. else
  110. Log::ActiveStorage::Metadata.new(
  111. storage: service_name.to_sym,
  112. file_id: event.payload[:key]&.to_s,
  113. metadata: event.payload[:metadata]
  114. )
  115. end
  116. # Log structured data
  117. LogStruct.info(log_data)
  118. end
  119. end
  120. end
  121. end

lib/log_struct/integrations/ahoy.rb

95.24% lines covered

21 relevant lines. 20 lines covered and 1 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. 3 return nil unless defined?(::Ahoy)
  12. 1 if defined?(::Ahoy::Tracker)
  13. 1 mod = Module.new do
  14. 1 extend T::Sig
  15. 2 sig { params(name: T.untyped, properties: T.nilable(T::Hash[T.untyped, T.untyped]), options: T.untyped).returns(T.untyped) }
  16. 1 def track(name, properties = nil, options = {})
  17. 1 result = super
  18. begin
  19. # Emit a lightweight structured log about the analytics event
  20. data = {
  21. 1 ahoy_event: T.let(name, T.untyped)
  22. }
  23. 1 data[:properties] = properties if properties
  24. 1 LogStruct.info(
  25. LogStruct::Log::Ahoy.new(
  26. message: "ahoy.track",
  27. ahoy_event: T.must(T.let(name, T.nilable(String))),
  28. properties: T.let(
  29. 2 properties && properties.transform_keys { |k| k.to_sym },
  30. T.nilable(T::Hash[Symbol, T.untyped])
  31. )
  32. )
  33. )
  34. rescue => e
  35. # Never raise from logging; rely on global error handling policies
  36. LogStruct.handle_exception(e, source: LogStruct::Source::App, context: {integration: :ahoy})
  37. end
  38. 1 result
  39. end
  40. end
  41. 1 T.unsafe(::Ahoy::Tracker).prepend(mod)
  42. end
  43. 1 true
  44. end
  45. end
  46. end
  47. end

lib/log_struct/integrations/carrierwave.rb

45.95% lines covered

37 relevant lines. 17 lines covered and 20 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. 4 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  16. 2 def self.setup(config)
  17. 2 return nil unless defined?(::CarrierWave)
  18. return nil unless config.enabled
  19. return nil unless config.integrations.enable_carrierwave
  20. # Patch CarrierWave to add logging
  21. ::CarrierWave::Uploader::Base.prepend(LoggingMethods)
  22. true
  23. end
  24. # Methods to add logging to CarrierWave operations
  25. 2 module LoggingMethods
  26. 2 extend T::Sig
  27. 2 extend T::Helpers
  28. 2 requires_ancestor { ::CarrierWave::Uploader::Base }
  29. # Log file storage operations
  30. 2 sig { params(args: T.untyped).returns(T.untyped) }
  31. 2 def store!(*args)
  32. start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  33. result = super
  34. duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
  35. # Extract file information
  36. file_size = file.size if file.respond_to?(:size)
  37. {
  38. identifier: identifier,
  39. filename: file.filename,
  40. content_type: file.content_type,
  41. size: file_size,
  42. store_path: store_path,
  43. extension: file.extension
  44. }
  45. # Log the store operation with structured data
  46. log_data = Log::CarrierWave::Upload.new(
  47. storage: storage.class.name.split("::").last.downcase.to_sym,
  48. file_id: identifier,
  49. filename: file.filename,
  50. mime_type: file.content_type,
  51. size: file_size,
  52. duration_ms: (duration * 1000.0).to_f,
  53. uploader: self.class.name,
  54. model: model.class.name,
  55. mount_point: mounted_as.to_s,
  56. version: version_name.to_s,
  57. store_path: store_path,
  58. extension: file.extension
  59. )
  60. ::Rails.logger.info(log_data)
  61. result
  62. end
  63. # Log file retrieve operations
  64. 2 sig { params(identifier: T.untyped, args: T.untyped).returns(T.untyped) }
  65. 2 def retrieve_from_store!(identifier, *args)
  66. Process.clock_gettime(Process::CLOCK_MONOTONIC)
  67. result = super
  68. Process.clock_gettime(Process::CLOCK_MONOTONIC)
  69. # Extract file information if available
  70. file_size = file.size if file&.respond_to?(:size)
  71. # Log the retrieve operation with structured data
  72. log_data = Log::CarrierWave::Download.new(
  73. storage: storage.class.name.split("::").last.downcase.to_sym,
  74. file_id: identifier,
  75. filename: file&.filename,
  76. mime_type: file&.content_type,
  77. size: file_size,
  78. # No duration field on Download event schema
  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. ::Rails.logger.info(log_data)
  87. result
  88. end
  89. end
  90. end
  91. end
  92. end

lib/log_struct/integrations/dotenv.rb

64.97% lines covered

157 relevant lines. 102 lines covered and 55 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 { void }
  31. 2 def subscribe!
  32. # Guard against double subscription
  33. 4 return if STATE.subscribed
  34. 2 instrumenter = defined?(::ActiveSupport::Notifications) ? ::ActiveSupport::Notifications : nil
  35. 2 return unless instrumenter
  36. 2 instrumenter.subscribe("load.dotenv") do |*args|
  37. # Allow tests to stub Log::Dotenv.new to force an error path
  38. LogStruct::Log::Dotenv.new
  39. event = ::ActiveSupport::Notifications::Event.new(*args)
  40. env = event.payload[:env]
  41. abs = env.filename
  42. file = begin
  43. if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
  44. Pathname.new(abs).relative_path_from(Pathname.new(::Rails.root.to_s)).to_s
  45. else
  46. abs
  47. end
  48. rescue
  49. abs
  50. end
  51. ts = event.time ? Time.at(event.time) : Time.now
  52. LogStruct.info(Log::Dotenv::Load.new(file: file, timestamp: ts))
  53. rescue => e
  54. if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env == "test"
  55. raise
  56. else
  57. LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
  58. end
  59. end
  60. 2 instrumenter.subscribe("update.dotenv") do |*args|
  61. LogStruct::Log::Dotenv.new
  62. event = ::ActiveSupport::Notifications::Event.new(*args)
  63. diff = event.payload[:diff]
  64. vars = diff.env.keys.map(&:to_s)
  65. ts = event.time ? Time.at(event.time) : Time.now
  66. LogStruct.debug(Log::Dotenv::Update.new(vars: vars, timestamp: ts))
  67. rescue => e
  68. if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env == "test"
  69. raise
  70. else
  71. LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
  72. end
  73. end
  74. 2 instrumenter.subscribe("save.dotenv") do |*args|
  75. 1 LogStruct::Log::Dotenv.new
  76. 1 event = ::ActiveSupport::Notifications::Event.new(*args)
  77. 1 ts = event.time ? Time.at(event.time) : Time.now
  78. 1 LogStruct.info(Log::Dotenv::Save.new(snapshot: true, timestamp: ts))
  79. rescue => e
  80. if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env == "test"
  81. raise
  82. else
  83. LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
  84. end
  85. end
  86. 2 instrumenter.subscribe("restore.dotenv") do |*args|
  87. LogStruct::Log::Dotenv.new
  88. event = ::ActiveSupport::Notifications::Event.new(*args)
  89. diff = event.payload[:diff]
  90. vars = diff.env.keys.map(&:to_s)
  91. ts = event.time ? Time.at(event.time) : Time.now
  92. LogStruct.info(Log::Dotenv::Restore.new(vars: vars, timestamp: ts))
  93. rescue => e
  94. if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env == "test"
  95. raise
  96. else
  97. LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
  98. end
  99. end
  100. 2 STATE.subscribed = true
  101. end
  102. end
  103. # Early boot subscription to buffer structured logs until logger is ready
  104. 2 @@boot_subscribed = T.let(false, T::Boolean)
  105. 4 sig { void }
  106. 2 def self.setup_boot
  107. 2 return if @@boot_subscribed
  108. 2 return unless defined?(::ActiveSupport::Notifications)
  109. 2 instrumenter = if Object.const_defined?(:Dotenv)
  110. 1 dm = T.unsafe(Object.const_get(:Dotenv))
  111. 1 dm.respond_to?(:instrumenter) ? T.unsafe(dm).instrumenter : ::ActiveSupport::Notifications
  112. else
  113. 1 ::ActiveSupport::Notifications
  114. end
  115. 2 instrumenter.subscribe("load.dotenv") do |*args|
  116. 1 event = ::ActiveSupport::Notifications::Event.new(*args)
  117. 1 env = event.payload[:env]
  118. 1 abs = env.filename
  119. file = begin
  120. 1 if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
  121. 1 Pathname.new(abs).relative_path_from(Pathname.new(::Rails.root.to_s)).to_s
  122. else
  123. abs
  124. end
  125. rescue
  126. abs
  127. end
  128. 1 ts = event.time ? Time.at(event.time) : Time.now
  129. 1 LogStruct::BootBuffer.add(Log::Dotenv::Load.new(file: file, timestamp: ts))
  130. rescue => e
  131. LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
  132. end
  133. 2 instrumenter.subscribe("update.dotenv") do |*args|
  134. 1 event = ::ActiveSupport::Notifications::Event.new(*args)
  135. 1 diff = event.payload[:diff]
  136. 1 vars = diff.env.keys.map(&:to_s)
  137. 1 ts = event.time ? Time.at(event.time) : Time.now
  138. 1 LogStruct::BootBuffer.add(Log::Dotenv::Update.new(vars: vars, timestamp: ts))
  139. rescue => e
  140. LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
  141. end
  142. 2 instrumenter.subscribe("save.dotenv") do |*args|
  143. 1 event = ::ActiveSupport::Notifications::Event.new(*args)
  144. 1 ts = event.time ? Time.at(event.time) : Time.now
  145. 1 LogStruct::BootBuffer.add(Log::Dotenv::Save.new(snapshot: true, timestamp: ts))
  146. rescue => e
  147. LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
  148. end
  149. 2 instrumenter.subscribe("restore.dotenv") do |*args|
  150. event = ::ActiveSupport::Notifications::Event.new(*args)
  151. diff = event.payload[:diff]
  152. vars = diff.env.keys.map(&:to_s)
  153. ts = event.time ? Time.at(event.time) : Time.now
  154. LogStruct::BootBuffer.add(Log::Dotenv::Restore.new(vars: vars, timestamp: ts))
  155. rescue => e
  156. LogStruct.handle_exception(e, source: LogStruct::Source::Dotenv)
  157. end
  158. 2 @@boot_subscribed = true
  159. end
  160. # Intercept Dotenv::Rails#logger= to defer replay until we resolve policy
  161. 4 sig { void }
  162. 2 def self.intercept_logger_setter!
  163. 2 return unless Object.const_defined?(:Dotenv)
  164. # Do not intercept when LogStruct is disabled; allow original dotenv replay
  165. 1 return unless LogStruct.enabled?
  166. 1 dotenv_mod = T.unsafe(Object.const_get(:Dotenv))
  167. 1 return unless dotenv_mod.const_defined?(:Rails)
  168. 1 klass = T.unsafe(dotenv_mod.const_get(:Rails))
  169. 1 return if klass.instance_variable_defined?(:@_logstruct_replay_patched)
  170. 1 original = klass.instance_method(:logger=)
  171. 1 @original_logger_setter = original
  172. 1 mod = Module.new do
  173. 1 define_method :logger= do |new_logger|
  174. # Defer replay: store desired logger, keep ReplayLogger as current
  175. 1 instance_variable_set(:@logstruct_pending_dotenv_logger, new_logger)
  176. 1 new_logger
  177. end
  178. 1 define_method :logstruct_pending_dotenv_logger do
  179. 1 instance_variable_get(:@logstruct_pending_dotenv_logger)
  180. end
  181. end
  182. 1 klass.prepend(mod)
  183. 1 klass.instance_variable_set(:@_logstruct_replay_patched, true)
  184. end
  185. # Decide which boot logs to emit after user initializers
  186. 4 sig { void }
  187. 2 def self.resolve_boot_logs!
  188. # If LogStruct is disabled, do not alter dotenv behavior at all
  189. 2 return unless LogStruct.enabled?
  190. 2 dotenv_mod = Object.const_defined?(:Dotenv) ? T.unsafe(Object.const_get(:Dotenv)) : nil
  191. 2 klass = dotenv_mod&.const_defined?(:Rails) ? T.unsafe(dotenv_mod.const_get(:Rails)) : nil
  192. 2 pending_logger = nil
  193. 2 railtie_instance = nil
  194. 2 if klass&.respond_to?(:instance)
  195. 1 railtie_instance = klass.instance
  196. 1 if railtie_instance.respond_to?(:logstruct_pending_dotenv_logger)
  197. 1 pending_logger = T.unsafe(railtie_instance).logstruct_pending_dotenv_logger
  198. end
  199. end
  200. 2 if LogStruct.enabled? && LogStruct.config.integrations.enable_dotenv
  201. # Structured path
  202. 2 if pending_logger && railtie_instance
  203. # Clear any buffered original logs
  204. 1 current_logger = railtie_instance.logger if railtie_instance.respond_to?(:logger)
  205. 1 if current_logger && current_logger.class.name.end_with?("ReplayLogger")
  206. begin
  207. 1 logs = current_logger.instance_variable_get(:@logs)
  208. 1 logs.clear if logs.respond_to?(:clear)
  209. rescue
  210. # best effort
  211. end
  212. end
  213. 1 railtie_instance.config.dotenv.logger = pending_logger
  214. end
  215. # Detach original subscriber and subscribe runtime structured
  216. 2 if dotenv_mod&.const_defined?(:LogSubscriber)
  217. 1 T.unsafe(dotenv_mod.const_get(:LogSubscriber)).detach_from(:dotenv)
  218. end
  219. 2 LogStruct::Integrations::Dotenv.subscribe!
  220. 2 require_relative "../boot_buffer"
  221. 2 LogStruct::BootBuffer.flush
  222. else
  223. # Original path: replay dotenv lines, drop structured buffer
  224. if railtie_instance && @original_logger_setter
  225. setter = @original_logger_setter
  226. new_logger = pending_logger
  227. if new_logger.nil? && ENV["RAILS_LOG_TO_STDOUT"].to_s.strip != ""
  228. require "logger"
  229. require "active_support/tagged_logging"
  230. new_logger = ActiveSupport::TaggedLogging.new(::Logger.new($stdout)).tagged("dotenv")
  231. end
  232. setter.bind_call(railtie_instance, new_logger) if new_logger
  233. end
  234. require_relative "../boot_buffer"
  235. LogStruct::BootBuffer.clear
  236. end
  237. end
  238. end
  239. end
  240. end
  241. # Subscribe immediately to capture earliest dotenv events into BootBuffer
  242. 2 LogStruct::Integrations::Dotenv.setup_boot
  243. # rubocop:enable Sorbet/ConstantsFromStrings

lib/log_struct/integrations/good_job.rb

53.13% lines covered

32 relevant lines. 17 lines covered and 15 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 require_relative "good_job/log_subscriber" if defined?(::GoodJob)
  10. 2 module LogStruct
  11. 2 module Integrations
  12. # GoodJob integration for structured logging
  13. #
  14. # GoodJob is a PostgreSQL-based ActiveJob backend that provides reliable,
  15. # scalable job processing for Rails applications. This integration provides
  16. # structured logging for all GoodJob operations.
  17. #
  18. # ## Features:
  19. # - Structured logging for job execution lifecycle
  20. # - Error tracking and retry logging
  21. # - Performance metrics and timing data
  22. # - Database operation logging
  23. # - Thread and process tracking
  24. # - Custom GoodJob logger with LogStruct formatting
  25. #
  26. # ## Integration Points:
  27. # - Replaces GoodJob.logger with LogStruct-compatible logger
  28. # - Subscribes to GoodJob's ActiveSupport notifications
  29. # - Captures job execution events, errors, and performance metrics
  30. # - Logs database operations and connection information
  31. #
  32. # ## Configuration:
  33. # The integration is automatically enabled when GoodJob is detected and
  34. # LogStruct configuration allows it. It can be disabled by setting:
  35. #
  36. # ```ruby
  37. # config.integrations.enable_goodjob = false
  38. # ```
  39. 2 module GoodJob
  40. 2 extend T::Sig
  41. 2 extend IntegrationInterface
  42. # Set up GoodJob structured logging
  43. #
  44. # This method configures GoodJob to use LogStruct's structured logging
  45. # by replacing the default logger and subscribing to job events.
  46. #
  47. # @param config [LogStruct::Configuration] The LogStruct configuration
  48. # @return [Boolean, nil] Returns true if setup was successful, nil if skipped
  49. 4 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  50. 2 def self.setup(config)
  51. 5 return nil unless defined?(::GoodJob)
  52. return nil unless config.enabled
  53. return nil unless config.integrations.enable_goodjob
  54. # Replace GoodJob's logger with our structured logger
  55. configure_logger
  56. # Subscribe to GoodJob's ActiveSupport notifications
  57. subscribe_to_notifications
  58. true
  59. end
  60. # Configure GoodJob to use LogStruct's structured logger
  61. 2 sig { void }
  62. 2 def self.configure_logger
  63. return unless defined?(::GoodJob)
  64. # Use direct reference to avoid const_get - GoodJob is guaranteed to be defined here
  65. goodjob_module = T.unsafe(GoodJob)
  66. # Replace GoodJob.logger with our structured logger if GoodJob is available
  67. if goodjob_module.respond_to?(:logger=)
  68. goodjob_module.logger = LogStruct::Integrations::GoodJob::Logger.new("GoodJob")
  69. end
  70. # Configure error handling for thread errors if GoodJob supports it
  71. if goodjob_module.respond_to?(:on_thread_error=)
  72. goodjob_module.on_thread_error = ->(exception) do
  73. log_entry = LogStruct::Log::GoodJob::Error.new(
  74. error_class: exception.class.name,
  75. error_message: exception.message,
  76. backtrace: exception.backtrace,
  77. process_id: ::Process.pid,
  78. thread_id: Thread.current.object_id.to_s(36)
  79. )
  80. goodjob_module.logger.error(log_entry)
  81. end
  82. end
  83. end
  84. # Subscribe to GoodJob's ActiveSupport notifications
  85. 2 sig { void }
  86. 2 def self.subscribe_to_notifications
  87. return unless defined?(::GoodJob)
  88. # Subscribe to our custom log subscriber for GoodJob events
  89. LogStruct::Integrations::GoodJob::LogSubscriber.attach_to :good_job
  90. end
  91. 2 private_class_method :configure_logger
  92. 2 private_class_method :subscribe_to_notifications
  93. end
  94. end
  95. end

lib/log_struct/integrations/good_job/log_subscriber.rb

98.53% lines covered

68 relevant lines. 67 lines covered and 1 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. begin
  4. 1 require "active_support/log_subscriber"
  5. rescue LoadError
  6. # ActiveSupport is not available, log subscriber will be skipped
  7. end
  8. 1 require_relative "../../log/good_job"
  9. 1 require_relative "../../enums/event"
  10. 1 require_relative "../../enums/level"
  11. 1 module LogStruct
  12. 1 module Integrations
  13. 1 module GoodJob
  14. # LogSubscriber for GoodJob ActiveSupport notifications
  15. #
  16. # This subscriber captures GoodJob's ActiveSupport notifications and converts
  17. # them into structured LogStruct::Log::GoodJob entries. It provides detailed
  18. # logging for job lifecycle events, performance metrics, and error tracking.
  19. #
  20. # ## Supported Events:
  21. # - job.enqueue - Job queued for execution
  22. # - job.start - Job execution started
  23. # - job.finish - Job completed successfully
  24. # - job.error - Job failed with error
  25. # - job.retry - Job retry initiated
  26. # - job.schedule - Job scheduled for future execution
  27. #
  28. # ## Event Data Captured:
  29. # - Job identification (ID, class, queue)
  30. # - Execution context (arguments, priority, scheduled time)
  31. # - Performance metrics (execution time, wait time)
  32. # - Error information (class, message, backtrace)
  33. # - Process and thread information
  34. 1 class LogSubscriber < ::ActiveSupport::LogSubscriber
  35. 1 extend T::Sig
  36. # Job enqueued event
  37. 2 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  38. 1 def enqueue(event)
  39. 2 payload = T.let(event.payload, T::Hash[Symbol, T.untyped])
  40. 2 job = payload[:job]
  41. 2 base_fields = build_base_fields(job, payload)
  42. 2 ts = event.time ? Time.at(event.time) : Time.now
  43. 2 logger.info(Log::GoodJob::Enqueue.new(
  44. **base_fields.to_kwargs,
  45. 2 scheduled_at: (job&.scheduled_at ? Time.at(job.scheduled_at.to_i) : nil),
  46. duration_ms: event.duration.to_f,
  47. enqueue_caller: job&.enqueue_caller_location,
  48. timestamp: ts
  49. ))
  50. end
  51. # Job execution started event
  52. 2 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  53. 1 def start(event)
  54. 1 payload = T.let(event.payload, T::Hash[Symbol, T.untyped])
  55. 1 job = payload[:job]
  56. 1 execution = payload[:execution] || payload[:good_job_execution]
  57. 1 base_fields = build_base_fields(job, payload)
  58. 1 ts = event.time ? Time.at(event.time) : Time.now
  59. 1 logger.info(Log::GoodJob::Start.new(
  60. **base_fields.to_kwargs,
  61. wait_ms: begin
  62. 1 wt = execution&.wait_time || calculate_wait_time(execution)
  63. 1 wt ? (wt.to_f * 1000.0) : nil
  64. end,
  65. 1 scheduled_at: (job&.scheduled_at ? Time.at(job.scheduled_at.to_i) : nil),
  66. process_id: ::Process.pid,
  67. thread_id: Thread.current.object_id.to_s(36),
  68. timestamp: ts
  69. ))
  70. end
  71. # Job completed successfully event
  72. 2 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  73. 1 def finish(event)
  74. 1 payload = T.let(event.payload, T::Hash[Symbol, T.untyped])
  75. 1 job = payload[:job]
  76. 1 base_fields = build_base_fields(job, payload)
  77. 1 start_ts = event.time ? Time.at(event.time) : Time.now
  78. 1 end_ts = event.end ? Time.at(event.end) : Time.now
  79. 1 logger.info(Log::GoodJob::Finish.new(
  80. **base_fields.to_kwargs,
  81. duration_ms: event.duration.to_f,
  82. finished_at: end_ts,
  83. process_id: ::Process.pid,
  84. thread_id: Thread.current.object_id.to_s(36),
  85. result: payload[:result]&.to_s,
  86. timestamp: start_ts
  87. ))
  88. end
  89. # Job failed with error event
  90. 2 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  91. 1 def error(event)
  92. 2 payload = T.let(event.payload, T::Hash[Symbol, T.untyped])
  93. 2 job = payload[:job]
  94. 2 execution = payload[:execution] || payload[:good_job_execution]
  95. 2 exception = payload[:exception] || payload[:error]
  96. 2 ts = event.time ? Time.at(event.time) : Time.now
  97. 2 base_fields = build_base_fields(job, payload)
  98. 2 logger.error(Log::GoodJob::Error.new(
  99. **base_fields.to_kwargs,
  100. exception_executions: execution&.exception_executions,
  101. error_class: exception&.class&.name,
  102. error_message: exception&.message,
  103. backtrace: exception&.backtrace,
  104. duration_ms: event.duration.to_f,
  105. process_id: ::Process.pid,
  106. thread_id: Thread.current.object_id.to_s(36),
  107. timestamp: ts
  108. ))
  109. end
  110. # Job scheduled for future execution event
  111. 2 sig { params(event: ::ActiveSupport::Notifications::Event).void }
  112. 1 def schedule(event)
  113. 1 payload = T.let(event.payload, T::Hash[Symbol, T.untyped])
  114. 1 job = payload[:job]
  115. 1 base_fields = build_base_fields(job, payload)
  116. 1 ts = event.time ? Time.at(event.time) : Time.now
  117. 1 logger.info(Log::GoodJob::Schedule.new(
  118. **base_fields.to_kwargs,
  119. 1 scheduled_at: (job&.scheduled_at ? Time.at(job.scheduled_at.to_i) : nil),
  120. priority: job&.priority,
  121. cron_key: job&.cron_key,
  122. duration_ms: event.duration.to_f,
  123. timestamp: ts
  124. ))
  125. end
  126. 1 private
  127. # Build BaseFields from job + payload (execution)
  128. 2 sig { params(job: T.untyped, payload: T::Hash[Symbol, T.untyped]).returns(Log::GoodJob::BaseFields) }
  129. 1 def build_base_fields(job, payload)
  130. 7 execution = payload[:execution] || payload[:good_job_execution]
  131. 7 Log::GoodJob::BaseFields.new(
  132. job_id: job&.job_id,
  133. job_class: job&.job_class,
  134. queue_name: job&.queue_name&.to_sym,
  135. arguments: job&.arguments,
  136. executions: execution&.executions
  137. )
  138. end
  139. # Calculate wait time from job creation to execution start
  140. 2 sig { params(execution: T.untyped).returns(T.nilable(Float)) }
  141. 1 def calculate_wait_time(execution)
  142. 2 return nil unless execution.respond_to?(:created_at)
  143. 2 return nil unless execution.respond_to?(:performed_at)
  144. 2 return nil unless execution.created_at && execution.performed_at
  145. 1 (execution.performed_at - execution.created_at).to_f
  146. rescue
  147. # Return nil if calculation fails
  148. nil
  149. end
  150. # Get the appropriate logger for GoodJob events
  151. 2 sig { returns(T.untyped) }
  152. 1 def logger
  153. # Always use Rails.logger - in production it will be configured by the integration setup,
  154. # in tests it will be set up by the test harness
  155. 7 Rails.logger
  156. end
  157. end
  158. end
  159. end
  160. end

lib/log_struct/integrations/good_job/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/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. # This logger extends LogStruct's SemanticLogger to provide optimal logging
  12. # performance while creating structured log entries specifically for GoodJob
  13. # operations and events.
  14. #
  15. # ## Benefits:
  16. # - High-performance logging with SemanticLogger backend
  17. # - Structured GoodJob-specific log entries
  18. # - Automatic job context capture
  19. # - Thread and process information
  20. # - Performance metrics and timing data
  21. #
  22. # ## Usage:
  23. # This logger is automatically configured when the GoodJob integration
  24. # is enabled. It replaces GoodJob.logger to provide structured logging
  25. # for all GoodJob operations.
  26. 1 class Logger < LogStruct::SemanticLogger::Logger
  27. 1 extend T::Sig
  28. # Override log methods to create GoodJob-specific log structs
  29. 1 %i[debug info warn error fatal].each do |level|
  30. 5 define_method(level) do |message = nil, payload = nil, &block|
  31. # Extract basic job context from thread-local variables
  32. 12 job_context = {}
  33. 12 if Thread.current[:good_job_execution]
  34. 2 execution = Thread.current[:good_job_execution]
  35. 2 if execution.respond_to?(:job_id)
  36. 2 job_context[:job_id] = execution.job_id
  37. 2 job_context[:job_class] = execution.job_class if execution.respond_to?(:job_class)
  38. 2 job_context[:queue_name] = execution.queue_name if execution.respond_to?(:queue_name)
  39. 2 job_context[:executions] = execution.executions if execution.respond_to?(:executions)
  40. 2 job_context[:scheduled_at] = execution.scheduled_at if execution.respond_to?(:scheduled_at)
  41. 2 job_context[:priority] = execution.priority if execution.respond_to?(:priority)
  42. end
  43. end
  44. 12 log_struct = Log::GoodJob::Log.new(
  45. 1 message: message || (block ? block.call : ""),
  46. process_id: ::Process.pid,
  47. thread_id: Thread.current.object_id.to_s(36),
  48. job_id: job_context[:job_id],
  49. job_class: job_context[:job_class],
  50. queue_name: job_context[:queue_name],
  51. executions: job_context[:executions],
  52. scheduled_at: job_context[:scheduled_at],
  53. priority: job_context[:priority]
  54. )
  55. 12 super(log_struct, payload, &nil)
  56. end
  57. end
  58. end
  59. end
  60. end
  61. end

lib/log_struct/integrations/host_authorization.rb

62.86% lines covered

35 relevant lines. 22 lines covered and 13 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. 2 return nil unless config.enabled
  32. 2 return nil unless config.integrations.enable_host_authorization
  33. # Define the response app as a separate variable to fix block alignment
  34. 2 response_app = lambda do |env|
  35. request = ::ActionDispatch::Request.new(env)
  36. # Include the blocked hosts app configuration in the log entry
  37. # This can be helpful later when reviewing logs.
  38. blocked_hosts = env["action_dispatch.blocked_hosts"]
  39. # Build allowed_hosts array
  40. allowed_hosts_array = T.let(nil, T.nilable(T::Array[String]))
  41. if blocked_hosts.respond_to?(:allowed_hosts)
  42. allowed_hosts_array = blocked_hosts.allowed_hosts
  43. end
  44. # Get allow_ip_hosts value
  45. allow_ip_hosts_value = T.let(nil, T.nilable(T::Boolean))
  46. if blocked_hosts.respond_to?(:allow_ip_hosts)
  47. allow_ip_hosts_value = blocked_hosts.allow_ip_hosts
  48. end
  49. # Create structured log entry for blocked host
  50. log_entry = LogStruct::Log::Security::BlockedHost.new(
  51. message: "Blocked host detected: #{request.host}",
  52. blocked_host: request.host,
  53. path: request.path,
  54. http_method: request.method,
  55. source_ip: request.ip,
  56. user_agent: request.user_agent,
  57. referer: request.referer,
  58. request_id: request.request_id,
  59. x_forwarded_for: request.x_forwarded_for,
  60. allowed_hosts: allowed_hosts_array&.empty? ? nil : allowed_hosts_array,
  61. allow_ip_hosts: allow_ip_hosts_value
  62. )
  63. # Log the blocked host
  64. LogStruct.warn(log_entry)
  65. # Use pre-defined headers and response if we are only logging or reporting
  66. # Dup the headers so they can be modified by downstream middleware
  67. [FORBIDDEN_STATUS, RESPONSE_HEADERS.dup, [RESPONSE_HTML]]
  68. end
  69. # Merge our response_app into existing host_authorization config to preserve excludes
  70. 2 existing = Rails.application.config.host_authorization
  71. 2 unless existing.is_a?(Hash)
  72. existing = {}
  73. end
  74. 2 existing = existing.dup
  75. 2 existing[:response_app] = response_app
  76. 2 Rails.application.config.host_authorization = existing
  77. 2 true
  78. end
  79. end
  80. end
  81. 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

69.39% lines covered

49 relevant lines. 34 lines covered and 15 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 class << self
  14. 2 extend T::Sig
  15. # Set up lograge for structured request logging
  16. 4 sig { override.params(logstruct_config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  17. 2 def setup(logstruct_config)
  18. 3 return nil unless defined?(::Lograge)
  19. 3 return nil unless logstruct_config.enabled
  20. 3 return nil unless logstruct_config.integrations.enable_lograge
  21. 3 configure_lograge(logstruct_config)
  22. 3 true
  23. end
  24. 2 private_class_method
  25. 4 sig { params(logstruct_config: LogStruct::Configuration).void }
  26. 2 def configure_lograge(logstruct_config)
  27. 3 ::Rails.application.configure do
  28. 3 config.lograge.enabled = true
  29. # Use a raw formatter that just returns the log struct.
  30. # The struct is converted to JSON by our Formatter (after filtering, etc.)
  31. 3 config.lograge.formatter = T.let(
  32. lambda do |data|
  33. # Coerce common fields to expected types
  34. 2 status = ((s = data[:status]) && s.respond_to?(:to_i)) ? s.to_i : s
  35. 2 duration_ms = ((d = data[:duration]) && d.respond_to?(:to_f)) ? d.to_f : d
  36. 2 view = ((v = data[:view]) && v.respond_to?(:to_f)) ? v.to_f : v
  37. 2 db = ((b = data[:db]) && b.respond_to?(:to_f)) ? b.to_f : b
  38. 2 params = data[:params]
  39. 2 params = params.deep_symbolize_keys if params&.respond_to?(:deep_symbolize_keys)
  40. 2 Log::Request.new(
  41. http_method: data[:method]&.to_s,
  42. path: data[:path]&.to_s,
  43. format: data[:format]&.to_sym,
  44. controller: data[:controller]&.to_s,
  45. action: data[:action]&.to_s,
  46. status: status,
  47. duration_ms: duration_ms,
  48. view: view,
  49. database: db,
  50. params: params,
  51. timestamp: Time.now
  52. )
  53. end,
  54. T.proc.params(hash: T::Hash[Symbol, T.untyped]).returns(Log::Request)
  55. )
  56. # Add custom options to lograge
  57. 3 config.lograge.custom_options = lambda do |event|
  58. Integrations::Lograge.lograge_default_options(event)
  59. end
  60. end
  61. end
  62. 2 sig { params(event: ActiveSupport::Notifications::Event).returns(T::Hash[Symbol, T.untyped]) }
  63. 2 def lograge_default_options(event)
  64. # Extract essential fields from the payload
  65. options = event.payload.slice(
  66. :request_id,
  67. :host,
  68. :source_ip
  69. ).compact
  70. if event.payload[:params].present?
  71. options[:params] = event.payload[:params].except("controller", "action")
  72. end
  73. # Process headers if available
  74. process_headers(event, options)
  75. # Apply custom options from application if provided
  76. apply_custom_options(event, options)
  77. options
  78. end
  79. # Process headers from the event payload
  80. 2 sig { params(event: ActiveSupport::Notifications::Event, options: T::Hash[Symbol, T.untyped]).void }
  81. 2 def process_headers(event, options)
  82. headers = event.payload[:headers]
  83. return if headers.blank?
  84. options[:user_agent] = headers["HTTP_USER_AGENT"]
  85. options[:content_type] = headers["CONTENT_TYPE"]
  86. options[:accept] = headers["HTTP_ACCEPT"]
  87. end
  88. # Apply custom options from the application's configuration
  89. 2 sig { params(event: ActiveSupport::Notifications::Event, options: T::Hash[Symbol, T.untyped]).void }
  90. 2 def apply_custom_options(event, options)
  91. custom_options_proc = LogStruct.config.integrations.lograge_custom_options
  92. return unless custom_options_proc&.respond_to?(:call)
  93. # Call the proc with the event and options
  94. # The proc can modify the options hash directly
  95. custom_options_proc.call(event, options)
  96. end
  97. end
  98. end
  99. end
  100. end

lib/log_struct/integrations/puma.rb

56.12% lines covered

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

lib/log_struct/integrations/rack_error_handler.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 "rack"
  4. 2 require "action_dispatch/middleware/show_exceptions"
  5. 2 require_relative "rack_error_handler/middleware"
  6. 2 module LogStruct
  7. 2 module Integrations
  8. # Rack middleware integration for structured logging
  9. 2 module RackErrorHandler
  10. 2 extend T::Sig
  11. 2 extend IntegrationInterface
  12. # Set up Rack middleware for structured error 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 config.enabled
  16. 2 return nil unless config.integrations.enable_rack_error_handler
  17. # Add structured logging middleware for security violations and errors
  18. # Need to insert before RemoteIp to catch IP spoofing errors it raises
  19. 2 ::Rails.application.middleware.insert_before(
  20. ::ActionDispatch::RemoteIp,
  21. Integrations::RackErrorHandler::Middleware
  22. )
  23. 2 true
  24. end
  25. end
  26. end
  27. end

lib/log_struct/integrations/rack_error_handler/middleware.rb

44.0% lines covered

50 relevant lines. 22 lines covered and 28 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 module LogStruct
  4. 2 module Integrations
  5. 2 module RackErrorHandler
  6. # Custom middleware to enhance Rails error logging with JSON format and request details
  7. 2 class Middleware
  8. 2 extend T::Sig
  9. # IP Spoofing error response
  10. 2 IP_SPOOF_HTML = T.let(
  11. "<html><head><title>IP Spoofing Detected</title></head><body>" \
  12. "<h1>Forbidden</h1>" \
  13. "<p>IP spoofing detected. This request has been blocked for security reasons.</p>" \
  14. "</body></html>",
  15. String
  16. )
  17. # CSRF error response
  18. 2 CSRF_HTML = T.let(
  19. "<html><head><title>CSRF Error</title></head><body>" \
  20. "<h1>Forbidden</h1>" \
  21. "<p>Invalid authenticity token. This request has been blocked to prevent cross-site request forgery.</p>" \
  22. "</body></html>",
  23. String
  24. )
  25. # Response headers calculated at load time
  26. 2 IP_SPOOF_HEADERS = T.let(
  27. {
  28. "Content-Type" => "text/html",
  29. "Content-Length" => IP_SPOOF_HTML.bytesize.to_s
  30. }.freeze,
  31. T::Hash[String, String]
  32. )
  33. 2 CSRF_HEADERS = T.let(
  34. {
  35. "Content-Type" => "text/html",
  36. "Content-Length" => CSRF_HTML.bytesize.to_s
  37. }.freeze,
  38. T::Hash[String, String]
  39. )
  40. # HTTP status code for forbidden responses
  41. 2 FORBIDDEN_STATUS = T.let(403, Integer)
  42. 4 sig { params(app: T.untyped).void }
  43. 2 def initialize(app)
  44. 2 @app = app
  45. end
  46. 2 sig { params(env: T.untyped).returns(T.untyped) }
  47. 2 def call(env)
  48. return @app.call(env) unless LogStruct.enabled?
  49. request = ::ActionDispatch::Request.new(env)
  50. begin
  51. # Trigger the same spoofing checks that ActionDispatch::RemoteIp performs after
  52. # it is initialized in the middleware stack. We run this manually because we
  53. # execute before that middleware and still want spoofing attacks to surface here.
  54. perform_remote_ip_check!(request)
  55. @app.call(env)
  56. rescue ::ActionDispatch::RemoteIp::IpSpoofAttackError => ip_spoof_error
  57. # Create a security log for IP spoofing
  58. security_log = Log::Security::IPSpoof.new(
  59. path: env["PATH_INFO"],
  60. http_method: env["REQUEST_METHOD"],
  61. user_agent: env["HTTP_USER_AGENT"],
  62. referer: env["HTTP_REFERER"],
  63. request_id: request.request_id,
  64. message: ip_spoof_error.message,
  65. client_ip: env["HTTP_CLIENT_IP"],
  66. x_forwarded_for: env["HTTP_X_FORWARDED_FOR"],
  67. timestamp: Time.now
  68. )
  69. ::Rails.logger.warn(security_log)
  70. [FORBIDDEN_STATUS, IP_SPOOF_HEADERS.dup, [IP_SPOOF_HTML]]
  71. rescue ::ActionController::InvalidAuthenticityToken => invalid_auth_token_error
  72. # Create a security log for CSRF error
  73. security_log = Log::Security::CSRFViolation.new(
  74. path: request.path,
  75. http_method: request.method,
  76. source_ip: request.remote_ip,
  77. user_agent: request.user_agent,
  78. referer: request.referer,
  79. request_id: request.request_id,
  80. message: invalid_auth_token_error.message,
  81. timestamp: Time.now
  82. )
  83. LogStruct.error(security_log)
  84. # Report to error reporting service and/or re-raise
  85. context = extract_request_context(env, request)
  86. LogStruct.handle_exception(invalid_auth_token_error, source: Source::Security, context: context)
  87. # If handle_exception raised an exception then Rails will deal with it (e.g. config.exceptions_app)
  88. # If we are only logging or reporting these security errors, then return a default response
  89. [FORBIDDEN_STATUS, CSRF_HEADERS.dup, [CSRF_HTML]]
  90. rescue => error
  91. # Extract request context for error reporting
  92. context = extract_request_context(env, request)
  93. # Create and log a structured exception with request context
  94. exception_log = Log.from_exception(Source::Rails, error, context)
  95. LogStruct.error(exception_log)
  96. # Re-raise any standard errors to let Rails or error reporter handle it.
  97. # Rails will also log the request details separately
  98. raise error
  99. end
  100. end
  101. 2 private
  102. 2 sig { params(request: ::ActionDispatch::Request).void }
  103. 2 def perform_remote_ip_check!(request)
  104. action_dispatch_config = ::Rails.application.config.action_dispatch
  105. check_ip = action_dispatch_config.ip_spoofing_check
  106. return unless check_ip
  107. proxies = normalized_trusted_proxies(action_dispatch_config.trusted_proxies)
  108. ::ActionDispatch::RemoteIp::GetIp
  109. .new(request, check_ip, proxies)
  110. .to_s
  111. end
  112. 2 sig { params(env: T::Hash[String, T.untyped], request: T.nilable(::ActionDispatch::Request)).returns(T::Hash[Symbol, T.untyped]) }
  113. 2 def extract_request_context(env, request = nil)
  114. request ||= ::ActionDispatch::Request.new(env)
  115. {
  116. request_id: request.request_id,
  117. path: request.path,
  118. method: request.method,
  119. user_agent: request.user_agent,
  120. referer: request.referer
  121. }
  122. rescue => error
  123. # If we can't extract request context, return minimal info
  124. {error_extracting_context: error.message}
  125. end
  126. 2 sig { params(configured_proxies: T.untyped).returns(T.untyped) }
  127. 2 def normalized_trusted_proxies(configured_proxies)
  128. if configured_proxies.nil? || (configured_proxies.respond_to?(:empty?) && configured_proxies.empty?)
  129. return ::ActionDispatch::RemoteIp::TRUSTED_PROXIES
  130. end
  131. return configured_proxies if configured_proxies.respond_to?(:any?)
  132. raise(
  133. ArgumentError,
  134. <<~EOM
  135. Setting config.action_dispatch.trusted_proxies to a single value isn't
  136. supported. Please set this to an enumerable instead. For
  137. example, instead of:
  138. config.action_dispatch.trusted_proxies = IPAddr.new("10.0.0.0/8")
  139. Wrap the value in an Array:
  140. config.action_dispatch.trusted_proxies = [IPAddr.new("10.0.0.0/8")]
  141. Note that passing an enumerable will *replace* the default set of trusted proxies.
  142. EOM
  143. )
  144. end
  145. end
  146. end
  147. end
  148. end

lib/log_struct/integrations/shrine.rb

25.71% lines covered

35 relevant lines. 9 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. # Set up Shrine structured logging
  15. 4 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  16. 2 def self.setup(config)
  17. 2 return nil unless defined?(::Shrine)
  18. return nil unless config.enabled
  19. return nil unless config.integrations.enable_shrine
  20. # Create a structured log subscriber for Shrine
  21. # ActiveSupport::Notifications::Event has name, time, end, transaction_id, payload, and duration
  22. shrine_log_subscriber = T.unsafe(lambda do |event|
  23. payload = event.payload.except(:io, :metadata, :name).dup
  24. # Map event name to Event type
  25. event_type = case event.name
  26. when :upload then Event::Upload
  27. when :download then Event::Download
  28. when :delete then Event::Delete
  29. when :metadata then Event::Metadata
  30. when :exists then Event::Exist # ActiveStorage uses 'exist', may as well use that
  31. else Event::Unknown
  32. end
  33. # Create structured log data
  34. # Ensure storage is always a symbol
  35. storage_sym = payload[:storage].to_sym
  36. log_data = case event_type
  37. when Event::Upload
  38. Log::Shrine::Upload.new(
  39. storage: storage_sym,
  40. location: payload[:location],
  41. uploader: payload[:uploader]&.to_s,
  42. upload_options: payload[:upload_options],
  43. options: payload[:options],
  44. duration_ms: event.duration.to_f
  45. )
  46. when Event::Download
  47. Log::Shrine::Download.new(
  48. storage: storage_sym,
  49. location: payload[:location],
  50. download_options: payload[:download_options]
  51. )
  52. when Event::Delete
  53. Log::Shrine::Delete.new(
  54. storage: storage_sym,
  55. location: payload[:location]
  56. )
  57. when Event::Metadata
  58. metadata_params = {
  59. storage: storage_sym,
  60. metadata: payload[:metadata]
  61. }
  62. metadata_params[:location] = payload[:location] if payload[:location]
  63. Log::Shrine::Metadata.new(**metadata_params)
  64. when Event::Exist
  65. Log::Shrine::Exist.new(
  66. storage: storage_sym,
  67. location: payload[:location],
  68. exist: payload[:exist]
  69. )
  70. else
  71. unknown_params = {storage: storage_sym, metadata: payload[:metadata]}
  72. unknown_params[:location] = payload[:location] if payload[:location]
  73. Log::Shrine::Metadata.new(**unknown_params)
  74. end
  75. # Pass the structured hash to the logger
  76. # If Rails.logger has our Formatter, it will handle JSON conversion
  77. ::Shrine.logger.info log_data
  78. end)
  79. # Configure Shrine to use our structured log subscriber
  80. ::Shrine.plugin :instrumentation,
  81. events: %i[upload exists download delete],
  82. log_subscriber: shrine_log_subscriber
  83. true
  84. end
  85. end
  86. end
  87. end

lib/log_struct/integrations/sidekiq.rb

58.82% lines covered

17 relevant lines. 10 lines covered and 7 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 require_relative "sidekiq/logger" if defined?(::Sidekiq)
  9. 2 module LogStruct
  10. 2 module Integrations
  11. # Sidekiq integration for structured logging
  12. 2 module Sidekiq
  13. 2 extend T::Sig
  14. 2 extend IntegrationInterface
  15. # Set up Sidekiq structured logging
  16. 4 sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
  17. 2 def self.setup(config)
  18. 2 return nil unless defined?(::Sidekiq)
  19. return nil unless config.enabled
  20. return nil unless config.integrations.enable_sidekiq
  21. # Configure Sidekiq server (worker) to use our logger
  22. ::Sidekiq.configure_server do |sidekiq_config|
  23. sidekiq_config.logger = LogStruct::Integrations::Sidekiq::Logger.new("Sidekiq-Server")
  24. end
  25. # Configure Sidekiq client (Rails app) to use our logger
  26. ::Sidekiq.configure_client do |sidekiq_config|
  27. sidekiq_config.logger = LogStruct::Integrations::Sidekiq::Logger.new("Sidekiq-Client")
  28. end
  29. true
  30. end
  31. end
  32. end
  33. end

lib/log_struct/integrations/sorbet.rb

92.68% lines covered

41 relevant lines. 38 lines covered and 3 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. 3 return nil unless config.integrations.enable_sorbet_error_handlers
  16. 3 clear_sig_error_handler!
  17. 3 install_error_handler!
  18. # Install inline type error handler
  19. # Called when T.let, T.cast, T.must, etc. fail
  20. 3 T::Configuration.inline_type_error_handler = lambda do |error, _opts|
  21. LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
  22. end
  23. # Install call validation error handler
  24. # Called when method signature validation fails
  25. 3 T::Configuration.call_validation_error_handler = lambda do |_signature, opts|
  26. 1 error = TypeError.new(opts[:pretty_message])
  27. 1 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. 3 T::Configuration.sig_builder_error_handler = lambda do |error, _location|
  32. LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
  33. end
  34. # Install sig validation error handler
  35. # Called when there's a problem with a signature validation
  36. 3 T::Configuration.sig_validation_error_handler = lambda do |error, _opts|
  37. LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
  38. end
  39. 3 true
  40. end
  41. 2 @installed = T.let(false, T::Boolean)
  42. 2 class << self
  43. 2 extend T::Sig
  44. 2 private
  45. 4 sig { void }
  46. 2 def install_error_handler!
  47. 3 return if installed?
  48. 3 T::Configuration.sig_builder_error_handler = lambda do |error, source|
  49. 1 LogStruct.handle_exception(error, source: source, context: nil)
  50. end
  51. 3 @installed = true
  52. end
  53. 2 sig do
  54. 2 returns(
  55. T.nilable(
  56. T.proc.params(error: StandardError, location: Thread::Backtrace::Location).void
  57. )
  58. )
  59. end
  60. 2 def clear_sig_error_handler!
  61. 5 previous_handler = T.cast(
  62. T::Configuration.instance_variable_get(:@sig_builder_error_handler),
  63. T.nilable(
  64. T.proc.params(error: StandardError, location: Thread::Backtrace::Location).void
  65. )
  66. )
  67. 5 T::Configuration.sig_builder_error_handler = nil
  68. 5 @installed = false
  69. 5 previous_handler
  70. end
  71. 4 sig { returns(T::Boolean) }
  72. 2 def installed?
  73. 3 @installed
  74. end
  75. end
  76. end
  77. end
  78. end

lib/log_struct/log.rb

100.0% lines covered

15 relevant lines. 15 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. # Dynamically require all top-level log structs under log/*
  11. # Nested per-event files are required by their parent files.
  12. 2 Dir[File.join(__dir__, "log", "*.rb")].sort.each do |file|
  13. 32 require file
  14. end
  15. 2 module LogStruct
  16. 2 module Log
  17. 2 extend T::Sig
  18. # Build an Error log from an exception with optional context and timestamp
  19. 2 sig do
  20. 1 params(
  21. source: Source,
  22. ex: StandardError,
  23. additional_data: T::Hash[T.any(String, Symbol), T.untyped],
  24. timestamp: Time
  25. ).returns(LogStruct::Log::Error)
  26. end
  27. 2 def self.from_exception(source, ex, additional_data = {}, timestamp = Time.now)
  28. 5 LogStruct::Log::Error.new(
  29. source: source,
  30. error_class: ex.class,
  31. message: ex.message,
  32. backtrace: ex.backtrace,
  33. additional_data: additional_data,
  34. timestamp: timestamp
  35. )
  36. end
  37. end
  38. 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. 9 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. 2 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. 1 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  44. 1 h[LogField::To] = to unless to.nil?
  45. 1 h[LogField::From] = from unless from.nil?
  46. 1 h[LogField::Subject] = subject unless subject.nil?
  47. 1 h[LogField::MessageId] = message_id unless message_id.nil?
  48. 1 h[LogField::MailerClass] = mailer_class unless mailer_class.nil?
  49. 1 h[LogField::MailerAction] = mailer_action unless mailer_action.nil?
  50. 1 h[LogField::AttachmentCount] = attachment_count unless attachment_count.nil?
  51. 1 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. 2 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. 1 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  44. 1 h[LogField::To] = to unless to.nil?
  45. 1 h[LogField::From] = from unless from.nil?
  46. 1 h[LogField::Subject] = subject unless subject.nil?
  47. 1 h[LogField::MessageId] = message_id unless message_id.nil?
  48. 1 h[LogField::MailerClass] = mailer_class unless mailer_class.nil?
  49. 1 h[LogField::MailerAction] = mailer_action unless mailer_action.nil?
  50. 1 h[LogField::AttachmentCount] = attachment_count unless attachment_count.nil?
  51. 1 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. 3 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. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  48. 2 h[LogField::To] = to unless to.nil?
  49. 2 h[LogField::From] = from unless from.nil?
  50. 2 h[LogField::Subject] = subject unless subject.nil?
  51. 2 h[LogField::MessageId] = message_id unless message_id.nil?
  52. 2 h[LogField::MailerClass] = mailer_class unless mailer_class.nil?
  53. 2 h[LogField::MailerAction] = mailer_action unless mailer_action.nil?
  54. 2 h[LogField::AttachmentCount] = attachment_count unless attachment_count.nil?
  55. 2 h[LogField::ErrorClass] = error_class
  56. 2 h[LogField::Message] = message
  57. 2 h[LogField::Backtrace] = backtrace unless backtrace.nil?
  58. 2 h
  59. end
  60. end
  61. end
  62. end
  63. end

lib/log_struct/log/active_job.rb

90.0% lines covered

20 relevant lines. 18 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_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 module LogStruct
  12. 2 module Log
  13. 2 class ActiveJob
  14. 2 class BaseFields < T::Struct
  15. 2 extend T::Sig
  16. 2 const :job_id, String
  17. 2 const :job_class, String
  18. 2 const :queue_name, T.nilable(Symbol), default: nil
  19. 2 const :arguments, T.nilable(T::Array[T.untyped]), default: nil
  20. 2 const :executions, T.nilable(Integer), default: nil
  21. 2 const :provider_job_id, T.nilable(String), default: nil
  22. 2 Kwargs = T.type_alias do
  23. {
  24. job_id: String,
  25. job_class: String,
  26. queue_name: T.nilable(Symbol),
  27. arguments: T.nilable(T::Array[T.untyped]),
  28. executions: T.nilable(Integer),
  29. provider_job_id: T.nilable(String)
  30. }
  31. end
  32. 2 sig { returns(Kwargs) }
  33. 2 def to_kwargs
  34. {
  35. job_id: job_id,
  36. job_class: job_class,
  37. queue_name: queue_name,
  38. arguments: arguments,
  39. executions: executions,
  40. provider_job_id: provider_job_id
  41. }
  42. end
  43. end
  44. end
  45. end
  46. end

lib/log_struct/log/active_job/enqueue.rb

76.92% lines covered

39 relevant lines. 30 lines covered and 9 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. 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 :retries, T.nilable(Integer), default: nil
  35. # Serialize shared fields
  36. 2 include LogStruct::Log::Interfaces::CommonFields
  37. 2 include LogStruct::Log::Shared::SerializeCommon
  38. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  39. 2 def to_h
  40. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  41. h[LogField::JobId] = job_id
  42. h[LogField::JobClass] = job_class
  43. h[LogField::QueueName] = queue_name unless queue_name.nil?
  44. h[LogField::Arguments] = arguments unless arguments.nil?
  45. h[LogField::Executions] = executions unless executions.nil?
  46. h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
  47. h[LogField::Retries] = retries unless retries.nil?
  48. h
  49. end
  50. end
  51. end
  52. end
  53. end

lib/log_struct/log/active_job/finish.rb

75.61% lines covered

41 relevant lines. 31 lines covered and 10 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. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  40. 2 def to_h
  41. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  42. h[LogField::JobId] = job_id
  43. h[LogField::JobClass] = job_class
  44. h[LogField::QueueName] = queue_name unless queue_name.nil?
  45. h[LogField::Arguments] = arguments unless arguments.nil?
  46. h[LogField::Executions] = executions unless executions.nil?
  47. h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
  48. h[LogField::DurationMs] = duration_ms
  49. h[LogField::FinishedAt] = finished_at
  50. h
  51. end
  52. end
  53. end
  54. end
  55. end

lib/log_struct/log/active_job/schedule.rb

76.92% lines covered

39 relevant lines. 30 lines covered and 9 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. 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 :scheduled_at, Time
  35. # Serialize shared fields
  36. 2 include LogStruct::Log::Interfaces::CommonFields
  37. 2 include LogStruct::Log::Shared::SerializeCommon
  38. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  39. 2 def to_h
  40. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  41. h[LogField::JobId] = job_id
  42. h[LogField::JobClass] = job_class
  43. h[LogField::QueueName] = queue_name unless queue_name.nil?
  44. h[LogField::Arguments] = arguments unless arguments.nil?
  45. h[LogField::Executions] = executions unless executions.nil?
  46. h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
  47. h[LogField::ScheduledAt] = scheduled_at
  48. h
  49. end
  50. end
  51. end
  52. end
  53. end

lib/log_struct/log/active_job/start.rb

75.61% lines covered

41 relevant lines. 31 lines covered and 10 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. 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 :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. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  40. 2 def to_h
  41. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  42. h[LogField::JobId] = job_id
  43. h[LogField::JobClass] = job_class
  44. h[LogField::QueueName] = queue_name unless queue_name.nil?
  45. h[LogField::Arguments] = arguments unless arguments.nil?
  46. h[LogField::Executions] = executions unless executions.nil?
  47. h[LogField::ProviderJobId] = provider_job_id unless provider_job_id.nil?
  48. h[LogField::StartedAt] = started_at
  49. h[LogField::Attempt] = attempt unless attempt.nil?
  50. 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. 2 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. 1 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  38. 1 h[LogField::Message] = message
  39. 1 h[LogField::Serializer] = serializer unless serializer.nil?
  40. 1 h[LogField::Adapter] = adapter unless adapter.nil?
  41. 1 h[LogField::ResourceClass] = resource_class unless resource_class.nil?
  42. 1 h[LogField::DurationMs] = duration_ms
  43. 1 h
  44. end
  45. end
  46. end
  47. end

lib/log_struct/log/active_storage.rb

89.47% lines covered

19 relevant lines. 17 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/stream"
  13. 2 require_relative "active_storage/url"
  14. 2 module LogStruct
  15. 2 module Log
  16. 2 class ActiveStorage
  17. 2 class BaseFields < T::Struct
  18. 2 extend T::Sig
  19. 2 const :storage, Symbol
  20. 2 const :file_id, String
  21. 2 Kwargs = T.type_alias do
  22. {
  23. storage: Symbol,
  24. file_id: String
  25. }
  26. end
  27. 2 sig { returns(Kwargs) }
  28. 2 def to_kwargs
  29. {
  30. storage: storage,
  31. file_id: file_id
  32. }
  33. end
  34. end
  35. end
  36. end
  37. end

lib/log_struct/log/active_storage/delete.rb

86.21% lines covered

29 relevant lines. 25 lines covered and 4 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. 2 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. # Serialize shared fields
  30. 2 include LogStruct::Log::Interfaces::CommonFields
  31. 2 include LogStruct::Log::Shared::SerializeCommon
  32. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  33. 2 def to_h
  34. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  35. h[LogField::Storage] = storage
  36. h[LogField::FileId] = file_id
  37. h
  38. end
  39. end
  40. end
  41. end
  42. end

lib/log_struct/log/active_storage/download.rb

80.0% lines covered

35 relevant lines. 28 lines covered and 7 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. 2 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. # Event-specific fields
  30. 2 const :filename, T.nilable(String), default: nil
  31. 2 const :range, T.nilable(String), default: nil
  32. 2 const :duration_ms, T.nilable(Float), default: nil
  33. # Serialize shared fields
  34. 2 include LogStruct::Log::Interfaces::CommonFields
  35. 2 include LogStruct::Log::Shared::SerializeCommon
  36. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  37. 2 def to_h
  38. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  39. h[LogField::Storage] = storage
  40. h[LogField::FileId] = file_id
  41. h[LogField::Filename] = filename unless filename.nil?
  42. h[LogField::Range] = range unless range.nil?
  43. h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
  44. h
  45. end
  46. end
  47. end
  48. end
  49. end

lib/log_struct/log/active_storage/exist.rb

83.87% lines covered

31 relevant lines. 26 lines covered and 5 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. 2 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. # 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. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  35. 2 def to_h
  36. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  37. h[LogField::Storage] = storage
  38. h[LogField::FileId] = file_id
  39. h[LogField::Exist] = exist unless exist.nil?
  40. h
  41. end
  42. end
  43. end
  44. end
  45. end

lib/log_struct/log/active_storage/metadata.rb

83.87% lines covered

31 relevant lines. 26 lines covered and 5 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. 2 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. # Event-specific fields
  30. 2 const :metadata, T.nilable(T::Hash[String, T.untyped]), default: nil
  31. # Serialize shared fields
  32. 2 include LogStruct::Log::Interfaces::CommonFields
  33. 2 include LogStruct::Log::Shared::SerializeCommon
  34. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  35. 2 def to_h
  36. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  37. h[LogField::Storage] = storage
  38. h[LogField::FileId] = file_id
  39. h[LogField::Metadata] = metadata unless metadata.nil?
  40. h
  41. end
  42. end
  43. end
  44. end
  45. end

lib/log_struct/log/active_storage/stream.rb

83.87% lines covered

31 relevant lines. 26 lines covered and 5 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 Stream < 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::Stream
  25. 2 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. # 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. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  35. 2 def to_h
  36. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  37. h[LogField::Storage] = storage
  38. h[LogField::FileId] = file_id
  39. h[LogField::Prefix] = prefix unless prefix.nil?
  40. h
  41. end
  42. end
  43. end
  44. end
  45. end

lib/log_struct/log/active_storage/upload.rb

75.61% lines covered

41 relevant lines. 31 lines covered and 10 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. 2 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. # Event-specific fields
  30. 2 const :filename, T.nilable(String), default: nil
  31. 2 const :mime_type, T.nilable(String), default: nil
  32. 2 const :size, T.nilable(Integer), default: nil
  33. 2 const :metadata, T.nilable(T::Hash[String, T.untyped]), default: nil
  34. 2 const :duration_ms, T.nilable(Float), default: nil
  35. 2 const :checksum, T.nilable(String), default: nil
  36. # Serialize shared fields
  37. 2 include LogStruct::Log::Interfaces::CommonFields
  38. 2 include LogStruct::Log::Shared::SerializeCommon
  39. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  40. 2 def to_h
  41. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  42. h[LogField::Storage] = storage
  43. h[LogField::FileId] = file_id
  44. h[LogField::Filename] = filename unless filename.nil?
  45. h[LogField::MimeType] = mime_type unless mime_type.nil?
  46. h[LogField::Size] = size unless size.nil?
  47. h[LogField::Metadata] = metadata unless metadata.nil?
  48. h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
  49. h[LogField::Checksum] = checksum unless checksum.nil?
  50. h
  51. end
  52. end
  53. end
  54. end
  55. end

lib/log_struct/log/active_storage/url.rb

83.87% lines covered

31 relevant lines. 26 lines covered and 5 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. 2 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. # Event-specific fields
  30. 2 const :url, String
  31. # Serialize shared fields
  32. 2 include LogStruct::Log::Interfaces::CommonFields
  33. 2 include LogStruct::Log::Shared::SerializeCommon
  34. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  35. 2 def to_h
  36. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  37. h[LogField::Storage] = storage
  38. h[LogField::FileId] = file_id
  39. h[LogField::Url] = url
  40. h
  41. end
  42. end
  43. end
  44. end
  45. 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. 3 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. 1 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  36. 1 h[LogField::Message] = message
  37. 1 h[LogField::AhoyEvent] = ahoy_event
  38. 1 h[LogField::Properties] = properties unless properties.nil?
  39. 1 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

75.61% lines covered

41 relevant lines. 31 lines covered and 10 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. 2 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. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  39. 2 def to_h
  40. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  41. h[LogField::Storage] = storage
  42. h[LogField::FileId] = file_id
  43. h[LogField::Uploader] = uploader unless uploader.nil?
  44. h[LogField::Model] = model unless model.nil?
  45. h[LogField::MountPoint] = mount_point unless mount_point.nil?
  46. h[LogField::Version] = version unless version.nil?
  47. h[LogField::StorePath] = store_path unless store_path.nil?
  48. h[LogField::Extension] = extension unless extension.nil?
  49. h
  50. end
  51. end
  52. end
  53. end
  54. end

lib/log_struct/log/carrierwave/download.rb

72.34% lines covered

47 relevant lines. 34 lines covered and 13 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. 2 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. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  43. 2 def to_h
  44. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  45. h[LogField::Storage] = storage
  46. h[LogField::FileId] = file_id
  47. h[LogField::Uploader] = uploader unless uploader.nil?
  48. h[LogField::Model] = model unless model.nil?
  49. h[LogField::MountPoint] = mount_point unless mount_point.nil?
  50. h[LogField::Version] = version unless version.nil?
  51. h[LogField::StorePath] = store_path unless store_path.nil?
  52. h[LogField::Extension] = extension unless extension.nil?
  53. h[LogField::Filename] = filename unless filename.nil?
  54. h[LogField::MimeType] = mime_type unless mime_type.nil?
  55. h[LogField::Size] = size unless size.nil?
  56. h
  57. end
  58. end
  59. end
  60. end
  61. end

lib/log_struct/log/carrierwave/upload.rb

70.59% lines covered

51 relevant lines. 36 lines covered and 15 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. 2 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. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  45. 2 def to_h
  46. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  47. h[LogField::Storage] = storage
  48. h[LogField::FileId] = file_id
  49. h[LogField::Uploader] = uploader unless uploader.nil?
  50. h[LogField::Model] = model unless model.nil?
  51. h[LogField::MountPoint] = mount_point unless mount_point.nil?
  52. h[LogField::Version] = version unless version.nil?
  53. h[LogField::StorePath] = store_path unless store_path.nil?
  54. h[LogField::Extension] = extension unless extension.nil?
  55. h[LogField::Filename] = filename unless filename.nil?
  56. h[LogField::MimeType] = mime_type unless mime_type.nil?
  57. h[LogField::Size] = size unless size.nil?
  58. h[LogField::Metadata] = metadata unless metadata.nil?
  59. h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
  60. 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. 2 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. 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::File] = file
  36. 2 h
  37. end
  38. end
  39. end
  40. end
  41. end

lib/log_struct/log/dotenv/restore.rb

88.89% lines covered

27 relevant lines. 24 lines covered and 3 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. 2 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. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  33. 2 def to_h
  34. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  35. h[LogField::Vars] = vars
  36. h
  37. end
  38. end
  39. end
  40. end
  41. end

lib/log_struct/log/dotenv/save.rb

88.89% lines covered

27 relevant lines. 24 lines covered and 3 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. 2 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. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  33. 2 def to_h
  34. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  35. h[LogField::Snapshot] = snapshot
  36. 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. 2 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/error.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 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. 5 const :timestamp, Time, factory: -> { Time.now }
  25. 2 const :level, Level, default: Level::Info
  26. # Event-specific fields
  27. 2 const :error_class, T.class_of(StandardError)
  28. 2 const :message, String
  29. 2 const :backtrace, T.nilable(T::Array[String]), default: nil
  30. # Additional data
  31. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  32. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  33. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  34. # Serialize shared fields
  35. 2 include LogStruct::Log::Interfaces::CommonFields
  36. 2 include LogStruct::Log::Shared::SerializeCommon
  37. 3 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  38. 2 def to_h
  39. 8 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  40. 8 h[LogField::ErrorClass] = error_class
  41. 8 h[LogField::Message] = message
  42. 8 h[LogField::Backtrace] = backtrace unless backtrace.nil?
  43. 8 h
  44. end
  45. end
  46. end
  47. end

lib/log_struct/log/good_job.rb

100.0% lines covered

21 relevant lines. 21 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 "good_job/log"
  8. 2 require_relative "good_job/enqueue"
  9. 2 require_relative "good_job/start"
  10. 2 require_relative "good_job/finish"
  11. 2 require_relative "good_job/error"
  12. 2 require_relative "good_job/schedule"
  13. 2 module LogStruct
  14. 2 module Log
  15. 2 class GoodJob
  16. 2 class BaseFields < T::Struct
  17. 2 extend T::Sig
  18. 2 const :job_id, T.nilable(String), default: nil
  19. 2 const :job_class, T.nilable(String), default: nil
  20. 2 const :queue_name, T.nilable(Symbol), default: nil
  21. 2 const :arguments, T.nilable(T::Array[T.untyped]), default: nil
  22. 2 const :executions, T.nilable(Integer), default: nil
  23. 2 Kwargs = T.type_alias do
  24. {
  25. 1 job_id: T.nilable(String),
  26. job_class: T.nilable(String),
  27. queue_name: T.nilable(Symbol),
  28. arguments: T.nilable(T::Array[T.untyped]),
  29. executions: T.nilable(Integer)
  30. }
  31. end
  32. 3 sig { returns(Kwargs) }
  33. 2 def to_kwargs
  34. {
  35. 7 job_id: job_id,
  36. job_class: job_class,
  37. queue_name: queue_name,
  38. arguments: arguments,
  39. executions: executions
  40. }
  41. end
  42. end
  43. end
  44. end
  45. end

lib/log_struct/log/good_job/enqueue.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 GoodJob
  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, 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 :duration_ms, Float
  34. 2 const :scheduled_at, T.nilable(Time), default: nil
  35. 2 const :enqueue_caller, T.nilable(String), 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. 4 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  42. 4 h[LogField::JobId] = job_id unless job_id.nil?
  43. 4 h[LogField::JobClass] = job_class unless job_class.nil?
  44. 4 h[LogField::QueueName] = queue_name unless queue_name.nil?
  45. 4 h[LogField::Arguments] = arguments unless arguments.nil?
  46. 4 h[LogField::Executions] = executions unless executions.nil?
  47. 4 h[LogField::DurationMs] = duration_ms
  48. 4 h[LogField::ScheduledAt] = scheduled_at unless scheduled_at.nil?
  49. 4 h[LogField::EnqueueCaller] = enqueue_caller unless enqueue_caller.nil?
  50. 4 h
  51. end
  52. end
  53. end
  54. end
  55. 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. 4 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. 4 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  46. 4 h[LogField::JobId] = job_id unless job_id.nil?
  47. 4 h[LogField::JobClass] = job_class unless job_class.nil?
  48. 4 h[LogField::QueueName] = queue_name unless queue_name.nil?
  49. 4 h[LogField::Arguments] = arguments unless arguments.nil?
  50. 4 h[LogField::Executions] = executions unless executions.nil?
  51. 4 h[LogField::ErrorClass] = error_class
  52. 4 h[LogField::ErrorMessage] = error_message
  53. 4 h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
  54. 4 h[LogField::ProcessId] = process_id
  55. 4 h[LogField::ThreadId] = thread_id
  56. 4 h[LogField::ExceptionExecutions] = exception_executions unless exception_executions.nil?
  57. 4 h[LogField::Backtrace] = backtrace unless backtrace.nil?
  58. 4 h
  59. end
  60. end
  61. end
  62. end
  63. end

lib/log_struct/log/good_job/finish.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 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. 5 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 :duration_ms, Float
  34. 2 const :finished_at, Time
  35. 2 const :process_id, Integer
  36. 2 const :thread_id, String
  37. 2 const :result, T.nilable(String), 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. 3 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  44. 3 h[LogField::JobId] = job_id unless job_id.nil?
  45. 3 h[LogField::JobClass] = job_class unless job_class.nil?
  46. 3 h[LogField::QueueName] = queue_name unless queue_name.nil?
  47. 3 h[LogField::Arguments] = arguments unless arguments.nil?
  48. 3 h[LogField::Executions] = executions unless executions.nil?
  49. 3 h[LogField::DurationMs] = duration_ms
  50. 3 h[LogField::FinishedAt] = finished_at
  51. 3 h[LogField::ProcessId] = process_id
  52. 3 h[LogField::ThreadId] = thread_id
  53. 3 h[LogField::Result] = result unless result.nil?
  54. 3 h
  55. end
  56. end
  57. end
  58. end
  59. 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. 17 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. 21 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  44. 21 h[LogField::JobId] = job_id unless job_id.nil?
  45. 21 h[LogField::JobClass] = job_class unless job_class.nil?
  46. 21 h[LogField::QueueName] = queue_name unless queue_name.nil?
  47. 21 h[LogField::Arguments] = arguments unless arguments.nil?
  48. 21 h[LogField::Executions] = executions unless executions.nil?
  49. 21 h[LogField::Message] = message
  50. 21 h[LogField::ProcessId] = process_id
  51. 21 h[LogField::ThreadId] = thread_id
  52. 21 h[LogField::ScheduledAt] = scheduled_at unless scheduled_at.nil?
  53. 21 h[LogField::Priority] = priority unless priority.nil?
  54. 21 h
  55. end
  56. end
  57. end
  58. end
  59. end

lib/log_struct/log/good_job/schedule.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 GoodJob
  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, 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 :duration_ms, Float
  34. 2 const :scheduled_at, Time
  35. 2 const :priority, T.nilable(Integer), default: nil
  36. 2 const :cron_key, T.nilable(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. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  43. 2 h[LogField::JobId] = job_id unless job_id.nil?
  44. 2 h[LogField::JobClass] = job_class unless job_class.nil?
  45. 2 h[LogField::QueueName] = queue_name unless queue_name.nil?
  46. 2 h[LogField::Arguments] = arguments unless arguments.nil?
  47. 2 h[LogField::Executions] = executions unless executions.nil?
  48. 2 h[LogField::DurationMs] = duration_ms
  49. 2 h[LogField::ScheduledAt] = scheduled_at
  50. 2 h[LogField::Priority] = priority unless priority.nil?
  51. 2 h[LogField::CronKey] = cron_key unless cron_key.nil?
  52. 2 h
  53. end
  54. end
  55. end
  56. end
  57. end

lib/log_struct/log/good_job/start.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 GoodJob
  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. 6 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 :process_id, Integer
  34. 2 const :thread_id, String
  35. 2 const :wait_ms, T.nilable(Float), default: nil
  36. 2 const :scheduled_at, T.nilable(Time), 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. 2 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  43. 2 h[LogField::JobId] = job_id unless job_id.nil?
  44. 2 h[LogField::JobClass] = job_class unless job_class.nil?
  45. 2 h[LogField::QueueName] = queue_name unless queue_name.nil?
  46. 2 h[LogField::Arguments] = arguments unless arguments.nil?
  47. 2 h[LogField::Executions] = executions unless executions.nil?
  48. 2 h[LogField::ProcessId] = process_id
  49. 2 h[LogField::ThreadId] = thread_id
  50. 2 h[LogField::WaitMs] = wait_ms unless wait_ms.nil?
  51. 2 h[LogField::ScheduledAt] = scheduled_at unless scheduled_at.nil?
  52. 2 h
  53. end
  54. end
  55. end
  56. end
  57. 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. 12 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. 815 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  38. 815 h[LogField::Message] = message
  39. 815 h
  40. end
  41. end
  42. end
  43. end

lib/log_struct/log/puma.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 "puma/start"
  8. 2 require_relative "puma/shutdown"

lib/log_struct/log/puma/shutdown.rb

90.0% lines covered

30 relevant lines. 27 lines covered and 3 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. 2 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. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  37. 2 def to_h
  38. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  39. h[LogField::ProcessId] = process_id unless process_id.nil?
  40. h
  41. end
  42. end
  43. end
  44. end
  45. end

lib/log_struct/log/puma/start.rb

76.09% lines covered

46 relevant lines. 35 lines covered and 11 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. 2 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. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  45. 2 def to_h
  46. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  47. h[LogField::Mode] = mode unless mode.nil?
  48. h[LogField::PumaVersion] = puma_version unless puma_version.nil?
  49. h[LogField::PumaCodename] = puma_codename unless puma_codename.nil?
  50. h[LogField::RubyVersion] = ruby_version unless ruby_version.nil?
  51. h[LogField::MinThreads] = min_threads unless min_threads.nil?
  52. h[LogField::MaxThreads] = max_threads unless max_threads.nil?
  53. h[LogField::Environment] = environment unless environment.nil?
  54. h[LogField::ProcessId] = process_id unless process_id.nil?
  55. h[LogField::ListeningAddresses] = listening_addresses unless listening_addresses.nil?
  56. h
  57. end
  58. end
  59. end
  60. end
  61. end

lib/log_struct/log/request.rb

100.0% lines covered

54 relevant lines. 54 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. 3 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. 2 const :request_id, T.nilable(String), default: nil
  32. # Event-specific fields
  33. 2 const :format, T.nilable(Symbol), default: nil
  34. 2 const :controller, T.nilable(String), default: nil
  35. 2 const :action, T.nilable(String), default: nil
  36. 2 const :status, T.nilable(Integer), default: nil
  37. 2 const :duration_ms, T.nilable(Float), default: nil
  38. 2 const :view, T.nilable(Float), default: nil
  39. 2 const :database, T.nilable(Float), default: nil
  40. 2 const :params, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
  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. 4 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  50. 4 h[LogField::Path] = path unless path.nil?
  51. 4 h[LogField::HttpMethod] = http_method unless http_method.nil?
  52. 4 h[LogField::SourceIp] = source_ip unless source_ip.nil?
  53. 4 h[LogField::UserAgent] = user_agent unless user_agent.nil?
  54. 4 h[LogField::Referer] = referer unless referer.nil?
  55. 4 h[LogField::RequestId] = request_id unless request_id.nil?
  56. 4 h[LogField::Format] = format unless format.nil?
  57. 4 h[LogField::Controller] = controller unless controller.nil?
  58. 4 h[LogField::Action] = action unless action.nil?
  59. 4 h[LogField::Status] = status unless status.nil?
  60. 4 h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
  61. 4 h[LogField::View] = view unless view.nil?
  62. 4 h[LogField::Database] = database unless database.nil?
  63. 4 h[LogField::Params] = params unless params.nil?
  64. 4 h
  65. end
  66. end
  67. end
  68. end

lib/log_struct/log/security.rb

89.47% lines covered

19 relevant lines. 17 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 const :request_id, T.nilable(String), default: nil
  21. 2 Kwargs = T.type_alias do
  22. {
  23. path: T.nilable(String),
  24. http_method: T.nilable(String),
  25. source_ip: T.nilable(String),
  26. user_agent: T.nilable(String),
  27. referer: T.nilable(String),
  28. request_id: T.nilable(String)
  29. }
  30. end
  31. 2 sig { returns(Kwargs) }
  32. 2 def to_kwargs
  33. {
  34. path: path,
  35. http_method: http_method,
  36. source_ip: source_ip,
  37. user_agent: user_agent,
  38. referer: referer,
  39. request_id: request_id
  40. }
  41. end
  42. end
  43. end
  44. end
  45. end

lib/log_struct/log/security/blocked_host.rb

74.07% lines covered

54 relevant lines. 40 lines covered and 14 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. 2 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. 2 const :request_id, T.nilable(String), default: nil
  33. # Event-specific fields
  34. 2 const :message, T.nilable(String), default: nil
  35. 2 const :blocked_host, T.nilable(String), default: nil
  36. 2 const :blocked_hosts, T.nilable(T::Array[String]), default: nil
  37. 2 const :x_forwarded_for, T.nilable(String), default: nil
  38. 2 const :allowed_hosts, T.nilable(T::Array[String]), default: nil
  39. 2 const :allow_ip_hosts, T.nilable(T::Boolean), default: nil
  40. # Additional data
  41. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  42. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  43. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  44. # Request fields (optional)
  45. 2 include LogStruct::Log::Interfaces::RequestFields
  46. # Serialize shared fields
  47. 2 include LogStruct::Log::Interfaces::CommonFields
  48. 2 include LogStruct::Log::Shared::SerializeCommon
  49. 2 include LogStruct::Log::Shared::AddRequestFields
  50. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  51. 2 def to_h
  52. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  53. h[LogField::Path] = path unless path.nil?
  54. h[LogField::HttpMethod] = http_method unless http_method.nil?
  55. h[LogField::SourceIp] = source_ip unless source_ip.nil?
  56. h[LogField::UserAgent] = user_agent unless user_agent.nil?
  57. h[LogField::Referer] = referer unless referer.nil?
  58. h[LogField::RequestId] = request_id unless request_id.nil?
  59. h[LogField::Message] = message unless message.nil?
  60. h[LogField::BlockedHost] = blocked_host unless blocked_host.nil?
  61. h[LogField::BlockedHosts] = blocked_hosts unless blocked_hosts.nil?
  62. h[LogField::XForwardedFor] = x_forwarded_for unless x_forwarded_for.nil?
  63. h[LogField::AllowedHosts] = allowed_hosts unless allowed_hosts.nil?
  64. h[LogField::AllowIpHosts] = allow_ip_hosts unless allow_ip_hosts.nil?
  65. h
  66. end
  67. end
  68. end
  69. end
  70. end

lib/log_struct/log/security/csrf_violation.rb

79.55% lines covered

44 relevant lines. 35 lines covered and 9 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. 2 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. 2 const :request_id, T.nilable(String), default: nil
  33. # Event-specific fields
  34. 2 const :message, T.nilable(String), default: nil
  35. # Additional data
  36. 2 include LogStruct::Log::Interfaces::AdditionalDataField
  37. 2 const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
  38. 2 include LogStruct::Log::Shared::MergeAdditionalDataFields
  39. # Request fields (optional)
  40. 2 include LogStruct::Log::Interfaces::RequestFields
  41. # Serialize shared fields
  42. 2 include LogStruct::Log::Interfaces::CommonFields
  43. 2 include LogStruct::Log::Shared::SerializeCommon
  44. 2 include LogStruct::Log::Shared::AddRequestFields
  45. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  46. 2 def to_h
  47. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  48. h[LogField::Path] = path unless path.nil?
  49. h[LogField::HttpMethod] = http_method unless http_method.nil?
  50. h[LogField::SourceIp] = source_ip unless source_ip.nil?
  51. h[LogField::UserAgent] = user_agent unless user_agent.nil?
  52. h[LogField::Referer] = referer unless referer.nil?
  53. h[LogField::RequestId] = request_id unless request_id.nil?
  54. h[LogField::Message] = message unless message.nil?
  55. h
  56. end
  57. end
  58. end
  59. end
  60. end

lib/log_struct/log/security/ip_spoof.rb

77.08% lines covered

48 relevant lines. 37 lines covered and 11 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. 2 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. 2 const :request_id, T.nilable(String), default: nil
  33. # Event-specific fields
  34. 2 const :message, T.nilable(String), default: nil
  35. 2 const :client_ip, T.nilable(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. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  48. 2 def to_h
  49. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  50. h[LogField::Path] = path unless path.nil?
  51. h[LogField::HttpMethod] = http_method unless http_method.nil?
  52. h[LogField::SourceIp] = source_ip unless source_ip.nil?
  53. h[LogField::UserAgent] = user_agent unless user_agent.nil?
  54. h[LogField::Referer] = referer unless referer.nil?
  55. h[LogField::RequestId] = request_id unless request_id.nil?
  56. h[LogField::Message] = message unless message.nil?
  57. h[LogField::ClientIp] = client_ip unless client_ip.nil?
  58. h[LogField::XForwardedFor] = x_forwarded_for unless x_forwarded_for.nil?
  59. h
  60. end
  61. end
  62. end
  63. end
  64. 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

86.21% lines covered

29 relevant lines. 25 lines covered and 4 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. 2 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. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  34. 2 def to_h
  35. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  36. h[LogField::Storage] = storage
  37. h[LogField::Location] = location
  38. h
  39. end
  40. end
  41. end
  42. end
  43. end

lib/log_struct/log/shrine/download.rb

83.87% lines covered

31 relevant lines. 26 lines covered and 5 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. 2 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. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  35. 2 def to_h
  36. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  37. h[LogField::Storage] = storage
  38. h[LogField::Location] = location
  39. h[LogField::DownloadOptions] = download_options unless download_options.nil?
  40. h
  41. end
  42. end
  43. end
  44. end
  45. end

lib/log_struct/log/shrine/exist.rb

83.87% lines covered

31 relevant lines. 26 lines covered and 5 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. 2 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 :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. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  35. 2 def to_h
  36. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  37. h[LogField::Storage] = storage
  38. h[LogField::Location] = location
  39. h[LogField::Exist] = exist unless exist.nil?
  40. h
  41. end
  42. end
  43. end
  44. end
  45. end

lib/log_struct/log/shrine/metadata.rb

83.87% lines covered

31 relevant lines. 26 lines covered and 5 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. 2 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, T.nilable(String), default: nil
  30. 2 const :metadata, T.nilable(T::Hash[String, T.untyped]), default: nil
  31. # Serialize shared fields
  32. 2 include LogStruct::Log::Interfaces::CommonFields
  33. 2 include LogStruct::Log::Shared::SerializeCommon
  34. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  35. 2 def to_h
  36. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  37. h[LogField::Storage] = storage
  38. h[LogField::Location] = location unless location.nil?
  39. h[LogField::Metadata] = metadata unless metadata.nil?
  40. h
  41. end
  42. end
  43. end
  44. end
  45. end

lib/log_struct/log/shrine/upload.rb

78.38% lines covered

37 relevant lines. 29 lines covered and 8 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. 2 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 :upload_options, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
  31. 2 const :options, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
  32. 2 const :uploader, T.nilable(String), default: nil
  33. 2 const :duration_ms, T.nilable(Float), default: nil
  34. # Serialize shared fields
  35. 2 include LogStruct::Log::Interfaces::CommonFields
  36. 2 include LogStruct::Log::Shared::SerializeCommon
  37. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  38. 2 def to_h
  39. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  40. h[LogField::Storage] = storage
  41. h[LogField::Location] = location
  42. h[LogField::UploadOptions] = upload_options unless upload_options.nil?
  43. h[LogField::Options] = options unless options.nil?
  44. h[LogField::Uploader] = uploader unless uploader.nil?
  45. h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
  46. h
  47. end
  48. end
  49. end
  50. end
  51. end

lib/log_struct/log/sidekiq.rb

81.25% lines covered

32 relevant lines. 26 lines covered and 6 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 < T::Struct
  20. 2 extend T::Sig
  21. # Shared/common fields
  22. 2 const :source, Source::Sidekiq, default: Source::Sidekiq
  23. 2 const :event, Event, default: Event::Log
  24. 2 const :timestamp, Time, factory: -> { Time.now }
  25. 2 const :level, Level, default: Level::Info
  26. # Event-specific fields
  27. 2 const :message, T.nilable(String), default: nil
  28. 2 const :context, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
  29. 2 const :process_id, T.nilable(Integer), default: nil
  30. 2 const :thread_id, T.nilable(T.any(Integer, String)), default: nil
  31. # Serialize shared fields
  32. 2 include LogStruct::Log::Interfaces::CommonFields
  33. 2 include LogStruct::Log::Shared::SerializeCommon
  34. 2 sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
  35. 2 def to_h
  36. h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  37. h[LogField::Message] = message unless message.nil?
  38. h[LogField::Context] = context unless context.nil?
  39. h[LogField::ProcessId] = process_id unless process_id.nil?
  40. h[LogField::ThreadId] = thread_id unless thread_id.nil?
  41. h
  42. end
  43. end
  44. end
  45. 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. 35 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. 10 h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
  49. 10 h[LogField::Message] = message
  50. 10 h[LogField::Sql] = sql
  51. 10 h[LogField::Name] = name
  52. 10 h[LogField::DurationMs] = duration_ms
  53. 10 h[LogField::RowCount] = row_count unless row_count.nil?
  54. 10 h[LogField::Adapter] = adapter unless adapter.nil?
  55. 10 h[LogField::BindParams] = bind_params unless bind_params.nil?
  56. 10 h[LogField::DatabaseName] = database_name unless database_name.nil?
  57. 10 h[LogField::ConnectionPoolSize] = connection_pool_size unless connection_pool_size.nil?
  58. 10 h[LogField::ActiveConnections] = active_connections unless active_connections.nil?
  59. 10 h[LogField::OperationType] = operation_type unless operation_type.nil?
  60. 10 h[LogField::TableNames] = table_names unless table_names.nil?
  61. 10 h
  62. end
  63. end
  64. end
  65. end

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

77.78% lines covered

18 relevant lines. 14 lines covered and 4 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "active_support/tagged_logging"
  4. # Monkey-patch ActiveSupport::TaggedLogging::Formatter to support hash inputs
  5. # This allows us to pass structured data to the logger and have tags incorporated
  6. # directly into the hash instead of being prepended as strings
  7. 2 module ActiveSupport
  8. 2 module TaggedLogging
  9. 2 extend T::Sig
  10. # Add class-level current_tags method for compatibility with Rails code
  11. # that expects to call ActiveSupport::TaggedLogging.current_tags
  12. # Use thread-local storage directly like Rails does internally
  13. 3 sig { returns(T::Array[T.any(String, Symbol)]) }
  14. 2 def self.current_tags
  15. 10 Thread.current[:activesupport_tagged_logging_tags] || []
  16. end
  17. 2 module FormatterExtension
  18. 2 extend T::Sig
  19. 2 extend T::Helpers
  20. 2 requires_ancestor { ::ActiveSupport::TaggedLogging::Formatter }
  21. # Override the call method to support hash input/output, and wrap
  22. # plain strings in a Hash under a `msg` key.
  23. # The data is then passed to our custom log formatter that transforms it
  24. # into a JSON string before logging.
  25. 2 sig { params(severity: T.any(String, Symbol), time: Time, progname: T.untyped, data: T.untyped).returns(String) }
  26. 2 def call(severity, time, progname, data)
  27. # Convert data to a hash if it's not already one
  28. data = {message: data.to_s} unless data.is_a?(Hash)
  29. # Add current tags to the hash if present
  30. # Use thread-local storage directly as fallback if current_tags method doesn't exist
  31. tags = T.unsafe(self).respond_to?(:current_tags) ? current_tags : (Thread.current[:activesupport_tagged_logging_tags] || [])
  32. data[:tags] = tags if tags.present?
  33. # Call the original formatter with our enhanced data
  34. super
  35. end
  36. end
  37. end
  38. end
  39. 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. 7 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. 7 @reporter_impl = case reporter_type
  60. when ErrorReporter
  61. reporter_type
  62. when Symbol
  63. 5 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. 15 @reporter_impl ||= detect_reporter
  107. end
  108. 3 sig { params(symbol: Symbol).returns(ErrorReporter) }
  109. 2 def resolve_symbol_reporter(symbol)
  110. 5 case symbol
  111. 1 when :sentry then ErrorReporter::Sentry
  112. 1 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

90.48% lines covered

63 relevant lines. 57 lines covered and 6 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "digest"
  4. 2 require_relative "hash_utils"
  5. 2 require_relative "config_struct/filters"
  6. 2 require_relative "enums/source"
  7. 2 module LogStruct
  8. # This class contains methods for filtering sensitive data in logs
  9. # It is used by Formatter to determine which keys should be filtered
  10. 2 class ParamFilters
  11. 2 class << self
  12. 2 extend T::Sig
  13. # Check if a key should be filtered based on our defined sensitive keys
  14. 4 sig { params(key: T.untyped, value: T.untyped).returns(T::Boolean) }
  15. 2 def should_filter_key?(key, value = nil)
  16. 4585 filters = LogStruct.config.filters
  17. 4585 normalized_key = key.to_s
  18. 4585 normalized_symbol = normalized_key.downcase.to_sym
  19. 4585 return true if filters.filter_keys.include?(normalized_symbol)
  20. 4580 filters.filter_matchers.any? do |matcher|
  21. 3036 matcher.matches?(normalized_key, value)
  22. rescue => e
  23. handle_filter_matcher_error(e, matcher, normalized_key)
  24. false
  25. end
  26. end
  27. # Check if a key should be hashed rather than completely filtered
  28. 3 sig { params(key: T.untyped).returns(T::Boolean) }
  29. 2 def should_include_string_hash?(key)
  30. 6 LogStruct.config.filters.filter_keys_with_hashes.include?(key.to_s.downcase.to_sym)
  31. end
  32. # Convert a value to a filtered summary hash (e.g. { _filtered: { class: "String", ... }})
  33. 3 sig { params(key: T.untyped, data: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
  34. 2 def summarize_json_attribute(key, data)
  35. 7 case data
  36. when Hash
  37. 1 summarize_hash(data)
  38. when Array
  39. 1 summarize_array(data)
  40. when String
  41. 4 summarize_string(data, should_include_string_hash?(key))
  42. else
  43. 1 {_class: data.class}
  44. end
  45. end
  46. # Summarize a String for logging, including details and an SHA256 hash (if configured)
  47. 3 sig { params(string: String, include_hash: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
  48. 2 def summarize_string(string, include_hash)
  49. filtered_string = {
  50. 6 _class: String
  51. }
  52. 6 if include_hash
  53. 2 filtered_string[:_hash] = HashUtils.hash_value(string)
  54. else
  55. 4 filtered_string[:_bytes] = string.bytesize
  56. end
  57. 6 filtered_string
  58. end
  59. # Summarize a Hash for logging, including details about the size and keys
  60. 3 sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
  61. 2 def summarize_hash(hash)
  62. 4 return {_class: "Hash", _empty: true} if hash.empty?
  63. # Don't include byte size if hash contains any filtered keys
  64. 4 has_sensitive_keys = T.let(false, T::Boolean)
  65. 4 normalized_keys = []
  66. 4 hash.each do |key, value|
  67. 6 has_sensitive_keys ||= should_filter_key?(key, value)
  68. 6 normalized_keys << normalize_summary_key(key)
  69. end
  70. summary = {
  71. 4 _class: Hash,
  72. _keys_count: hash.keys.size,
  73. _keys: normalized_keys.take(10)
  74. }
  75. # Only add byte size if no sensitive keys are present
  76. 4 summary[:_bytes] = hash.to_json.bytesize unless has_sensitive_keys
  77. 4 summary
  78. end
  79. # Summarize an Array for logging, including details about the size and items
  80. 3 sig { params(array: T::Array[T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
  81. 2 def summarize_array(array)
  82. 3 return {_class: "Array", _empty: true} if array.empty?
  83. {
  84. 2 _class: Array,
  85. _count: array.size,
  86. _bytes: array.to_json.bytesize
  87. }
  88. end
  89. 2 private
  90. 3 sig { params(key: T.any(String, Symbol, Integer, T.untyped)).returns(T.any(Symbol, String)) }
  91. 2 def normalize_summary_key(key)
  92. 6 if key.is_a?(Symbol)
  93. 5 key
  94. 1 elsif key.respond_to?(:to_sym)
  95. key.to_sym
  96. else
  97. 1 key.to_s
  98. end
  99. rescue
  100. key.to_s
  101. end
  102. 2 sig { params(error: StandardError, matcher: ConfigStruct::FilterMatcher, key: String).void }
  103. 2 def handle_filter_matcher_error(error, matcher, key)
  104. context = {
  105. matcher: matcher.label,
  106. key: key
  107. }
  108. LogStruct.handle_exception(error, source: Source::Internal, context: context)
  109. end
  110. end
  111. end
  112. end

lib/log_struct/rails_boot_banner_silencer.rb

93.1% lines covered

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

lib/log_struct/railtie.rb

55.26% lines covered

38 relevant lines. 21 lines covered and 17 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. # Configure early, right after logger initialization
  12. 2 initializer "logstruct.configure_logger", after: :initialize_logger do |app|
  13. 2 next unless LogStruct.enabled?
  14. # Use SemanticLogger for powerful logging features
  15. 2 LogStruct::SemanticLogger::Setup.configure_semantic_logger(app)
  16. end
  17. # Setup all integrations after logger setup is complete
  18. 2 initializer "logstruct.setup", before: :build_middleware_stack do |app|
  19. 2 next unless LogStruct.enabled?
  20. # Merge Rails filter parameters into our filters
  21. 2 LogStruct.merge_rails_filter_parameters!
  22. # Set up non-middleware integrations first
  23. 2 Integrations.setup_integrations(stage: :non_middleware)
  24. # Note: Host allowances are managed by the test app itself.
  25. end
  26. # Setup middleware integrations during Rails configuration (before middleware stack is built)
  27. # Must be done in the Railtie class body, not in an initializer
  28. 2 initializer "logstruct.configure_middleware", before: :build_middleware_stack do |app|
  29. # This runs before middleware stack is frozen, so we can configure it
  30. 2 next unless LogStruct.enabled?
  31. 2 Integrations.setup_integrations(stage: :middleware)
  32. end
  33. # Emit Puma lifecycle logs when running `rails server`
  34. 2 initializer "logstruct.puma_lifecycle", after: "logstruct.configure_logger" do
  35. 2 is_server = ::LogStruct.server_mode?
  36. 2 next unless is_server
  37. begin
  38. require "log_struct/log/puma"
  39. port = T.let(nil, T.nilable(String))
  40. ARGV.each_with_index do |arg, idx|
  41. if arg == "-p" || arg == "--port"
  42. port = ARGV[idx + 1]
  43. break
  44. elsif arg.start_with?("--port=")
  45. port = arg.split("=", 2)[1]
  46. break
  47. end
  48. end
  49. started = LogStruct::Log::Puma::Start.new(
  50. mode: "single",
  51. environment: (defined?(::Rails) && ::Rails.respond_to?(:env)) ? ::Rails.env : nil,
  52. process_id: Process.pid,
  53. listening_addresses: port ? ["tcp://127.0.0.1:#{port}"] : nil
  54. )
  55. begin
  56. warn("[logstruct] puma lifecycle init")
  57. rescue
  58. end
  59. LogStruct.info(started)
  60. at_exit do
  61. shutdown = LogStruct::Log::Puma::Shutdown.new(
  62. process_id: Process.pid
  63. )
  64. LogStruct.info(shutdown)
  65. end
  66. rescue
  67. # best-effort
  68. end
  69. end
  70. # Delegate integration initializers to Integrations module
  71. 2 LogStruct::Integrations.setup_initializers(self)
  72. end
  73. end

lib/log_struct/semantic_logger/color_formatter.rb

78.57% lines covered

84 relevant lines. 66 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 module LogStruct
  6. 2 module SemanticLogger
  7. # Development-Optimized Colorized JSON Formatter
  8. #
  9. # This formatter extends SemanticLogger's Color formatter to provide beautiful,
  10. # readable JSON output in development environments. It significantly improves
  11. # the developer experience when working with structured logs.
  12. #
  13. # ## Benefits of Colorized Output:
  14. #
  15. # ### Readability
  16. # - **Syntax highlighting**: JSON keys, values, and data types are color-coded
  17. # - **Visual hierarchy**: Different colors help identify structure at a glance
  18. # - **Error spotting**: Quickly identify malformed data or unexpected values
  19. # - **Context separation**: Log entries are visually distinct from each other
  20. #
  21. # ### Performance in Development
  22. # - **Faster debugging**: Quickly scan logs without reading every character
  23. # - **Pattern recognition**: Colors help identify common log patterns
  24. # - **Reduced cognitive load**: Less mental effort required to parse log output
  25. # - **Improved workflow**: Spend less time reading logs, more time coding
  26. #
  27. # ### Customization
  28. # - **Configurable colors**: Customize colors for keys, strings, numbers, etc.
  29. # - **Environment-aware**: Automatically disabled in production/CI environments
  30. # - **Fallback support**: Gracefully falls back to standard formatting if needed
  31. #
  32. # ## Color Mapping:
  33. # - **Keys**: Yellow - Easy to spot field names
  34. # - **Strings**: Green - Clear indication of text values
  35. # - **Numbers**: Blue - Numeric values stand out
  36. # - **Booleans**: Magenta - true/false values are distinctive
  37. # - **Null**: Red - Missing values are immediately visible
  38. # - **Logger names**: Cyan - Source identification
  39. #
  40. # ## Integration with SemanticLogger:
  41. # This formatter preserves all SemanticLogger benefits (performance, threading,
  42. # reliability) while adding visual enhancements. It processes LogStruct types,
  43. # hashes, and plain messages with appropriate colorization.
  44. #
  45. # The formatter is automatically enabled in development when `enable_color_output`
  46. # is true (default), providing zero-configuration enhanced logging experience.
  47. 2 class ColorFormatter < ::SemanticLogger::Formatters::Color
  48. 2 extend T::Sig
  49. 3 sig { params(color_map: T.nilable(T::Hash[Symbol, Symbol]), args: T.untyped).void }
  50. 2 def initialize(color_map: nil, **args)
  51. 8 super(**args)
  52. 8 @logstruct_formatter = T.let(LogStruct::Formatter.new, LogStruct::Formatter)
  53. # Set up custom color mapping
  54. 8 @custom_colors = T.let(color_map || default_color_map, T::Hash[Symbol, Symbol])
  55. end
  56. 3 sig { override.params(log: ::SemanticLogger::Log, logger: T.untyped).returns(String) }
  57. 2 def call(log, logger)
  58. # Handle LogStruct types specially with colorization
  59. 186 if log.payload.is_a?(LogStruct::Log::Interfaces::CommonFields)
  60. # Get the LogStruct formatted JSON
  61. logstruct_json = @logstruct_formatter.call(log.level, log.time, log.name, log.payload)
  62. # Parse and colorize it
  63. begin
  64. parsed_data = T.let(JSON.parse(logstruct_json), T::Hash[String, T.untyped])
  65. colorized_json = colorize_json(parsed_data)
  66. # Use SemanticLogger's prefix formatting but with our colorized content
  67. prefix = format("%<time>s %<level>s [%<process>s] %<name>s -- ",
  68. time: format_time(log.time),
  69. level: format_level(log.level),
  70. process: log.process_info,
  71. name: format_name(log.name))
  72. "#{prefix}#{colorized_json}\n"
  73. rescue JSON::ParserError
  74. # Fallback to standard formatting
  75. super
  76. end
  77. 186 elsif log.payload.is_a?(Hash) || log.payload.is_a?(T::Struct)
  78. # Process hashes through our formatter then colorize
  79. begin
  80. 16 logstruct_json = @logstruct_formatter.call(log.level, log.time, log.name, log.payload)
  81. 16 parsed_data = T.let(JSON.parse(logstruct_json), T::Hash[String, T.untyped])
  82. 16 colorized_json = colorize_json(parsed_data)
  83. 16 prefix = format("%<time>s %<level>s [%<process>s] %<name>s -- ",
  84. time: format_time(log.time),
  85. level: format_level(log.level),
  86. process: log.process_info,
  87. name: format_name(log.name))
  88. 16 "#{prefix}#{colorized_json}\n"
  89. rescue JSON::ParserError
  90. # Fallback to standard formatting
  91. super
  92. end
  93. else
  94. # For plain messages, use SemanticLogger's default colorization
  95. 170 super
  96. end
  97. end
  98. 2 private
  99. 2 sig { returns(LogStruct::Formatter) }
  100. 2 attr_reader :logstruct_formatter
  101. # Default color mapping for LogStruct JSON
  102. 3 sig { returns(T::Hash[Symbol, Symbol]) }
  103. 2 def default_color_map
  104. 7 {
  105. key: :yellow,
  106. string: :green,
  107. number: :blue,
  108. bool: :magenta,
  109. nil: :red,
  110. name: :cyan
  111. }
  112. end
  113. # Simple JSON colorizer that adds ANSI codes
  114. 3 sig { params(data: T::Hash[String, T.untyped]).returns(String) }
  115. 2 def colorize_json(data)
  116. # For now, just return a simple colorized version of the JSON
  117. # This is much simpler than the full recursive approach
  118. 16 json_str = JSON.pretty_generate(data)
  119. # Apply basic colorization with regex
  120. 16 json_str.gsub(/"([^"]+)":/, colorize_text('\1', :key) + ":")
  121. .gsub(/: "([^"]*)"/, ": " + colorize_text('\1', :string))
  122. .gsub(/: (\d+\.?\d*)/, ": " + colorize_text('\1', :number))
  123. .gsub(/: (true|false)/, ": " + colorize_text('\1', :bool))
  124. .gsub(": null", ": " + colorize_text("null", :nil))
  125. end
  126. # Add ANSI color codes to text
  127. 3 sig { params(text: String, color_type: Symbol).returns(String) }
  128. 2 def colorize_text(text, color_type)
  129. 80 color = @custom_colors[color_type] || :white
  130. 80 "\e[#{color_code_for(color)}m#{text}\e[0m"
  131. end
  132. # Format timestamp
  133. 3 sig { params(time: Time).returns(String) }
  134. 2 def format_time(time)
  135. 186 time.strftime("%Y-%m-%d %H:%M:%S.%6N")
  136. end
  137. # Format log level with color
  138. 3 sig { params(level: T.any(String, Symbol)).returns(String) }
  139. 2 def format_level(level)
  140. 16 level_str = level.to_s.upcase[0]
  141. 16 color = level_color_for(level.to_sym)
  142. 16 "\e[#{color_code_for(color)}m#{level_str}\e[0m"
  143. end
  144. # Format logger name with color
  145. 3 sig { params(name: T.nilable(String)).returns(String) }
  146. 2 def format_name(name)
  147. 16 return "" unless name
  148. 16 color = @custom_colors[:name] || :cyan
  149. 16 "\e[#{color_code_for(color)}m#{name}\e[0m"
  150. end
  151. # Get color for log level
  152. 3 sig { params(level: Symbol).returns(Symbol) }
  153. 2 def level_color_for(level)
  154. 16 case level
  155. 1 when :debug then :magenta
  156. 10 when :info then :cyan
  157. 1 when :warn then :yellow
  158. 3 when :error then :red
  159. 1 when :fatal then :red
  160. else :cyan
  161. end
  162. end
  163. # Get ANSI color code for color symbol
  164. 3 sig { params(color: Symbol).returns(String) }
  165. 2 def color_code_for(color)
  166. 112 case color
  167. when :black then "30"
  168. 20 when :red then "31"
  169. 16 when :green then "32"
  170. 16 when :yellow then "33"
  171. 16 when :blue then "34"
  172. 16 when :magenta then "35"
  173. 26 when :cyan then "36"
  174. 2 when :white then "37"
  175. when :bright_black then "90"
  176. when :bright_red then "91"
  177. when :bright_green then "92"
  178. when :bright_yellow then "93"
  179. when :bright_blue then "94"
  180. when :bright_magenta then "95"
  181. when :bright_cyan then "96"
  182. when :bright_white then "97"
  183. else "37" # default to white
  184. end
  185. end
  186. end
  187. end
  188. end

lib/log_struct/semantic_logger/concerns/log_methods.rb

95.16% lines covered

62 relevant lines. 59 lines covered and 3 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, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
  12. 2 def debug(message = nil, payload = nil, &block)
  13. 7 instrument_log(message, :debug)
  14. 7 result = if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct) || message.is_a?(Hash)
  15. 2 super(nil, payload: message, &block)
  16. else
  17. 5 super
  18. end
  19. 7 broadcasts.each do |logger|
  20. 1 next unless logger.respond_to?(:debug)
  21. 1 message.is_a?(String) ? logger.debug(message) : (logger.debug(&block) if block)
  22. end
  23. 7 result
  24. end
  25. 4 sig { params(message: T.untyped, payload: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
  26. 2 def info(message = nil, payload = nil, &block)
  27. 636 instrument_log(message, :info)
  28. 636 result = if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct) || message.is_a?(Hash)
  29. 29 super(nil, payload: message, &block)
  30. else
  31. 607 super
  32. end
  33. 636 broadcasts.each do |logger|
  34. 3 next unless logger.respond_to?(:info)
  35. 3 message.is_a?(String) ? logger.info(message) : (logger.info(&block) if block)
  36. end
  37. 636 result
  38. end
  39. 3 sig { params(message: T.untyped, payload: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
  40. 2 def warn(message = nil, payload = nil, &block)
  41. 3 instrument_log(message, :warn)
  42. 3 result = if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct) || message.is_a?(Hash)
  43. 2 super(nil, payload: message, &block)
  44. else
  45. 1 super
  46. end
  47. 3 broadcasts.each do |logger|
  48. 1 next unless logger.respond_to?(:warn)
  49. 1 message.is_a?(String) ? logger.warn(message) : (logger.warn(&block) if block)
  50. end
  51. 3 result
  52. end
  53. 3 sig { params(message: T.untyped, payload: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
  54. 2 def error(message = nil, payload = nil, &block)
  55. 10 instrument_log(message, :error)
  56. 10 result = if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct) || message.is_a?(Hash)
  57. 8 super(nil, payload: message, &block)
  58. else
  59. 2 super
  60. end
  61. 10 broadcasts.each do |logger|
  62. 1 next unless logger.respond_to?(:error)
  63. 1 message.is_a?(String) ? logger.error(message) : (logger.error(&block) if block)
  64. end
  65. 10 result
  66. end
  67. 3 sig { params(message: T.untyped, payload: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
  68. 2 def fatal(message = nil, payload = nil, &block)
  69. 2 instrument_log(message, :fatal)
  70. 2 result = if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct) || message.is_a?(Hash)
  71. 2 super(nil, payload: message, &block)
  72. else
  73. super
  74. end
  75. 2 broadcasts.each do |logger|
  76. next unless logger.respond_to?(:fatal)
  77. message.is_a?(String) ? logger.fatal(message) : (logger.fatal(&block) if block)
  78. end
  79. 2 result
  80. end
  81. 2 private
  82. # Instrument log events for subscribers
  83. 4 sig { params(message: T.untyped, level: Symbol).void }
  84. 2 def instrument_log(message, level)
  85. 658 return unless message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct)
  86. 36 ::ActiveSupport::Notifications.instrument("log.logstruct", log: message, level: level)
  87. end
  88. end
  89. end
  90. end
  91. end

lib/log_struct/semantic_logger/formatter.rb

96.3% lines covered

27 relevant lines. 26 lines covered and 1 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. # High-Performance JSON Formatter with LogStruct Integration
  8. #
  9. # This formatter extends SemanticLogger's JSON formatter to provide optimal
  10. # JSON serialization performance while preserving all LogStruct features
  11. # including data filtering, sensitive data scrubbing, and type-safe structures.
  12. #
  13. # ## Performance Advantages Over Rails Logger:
  14. #
  15. # ### Serialization Performance
  16. # - **Direct JSON generation**: Bypasses intermediate object creation
  17. # - **Streaming serialization**: Memory-efficient processing of large objects
  18. # - **Type-optimized paths**: Fast serialization for common data types
  19. # - **Zero-copy operations**: Minimal memory allocation during serialization
  20. #
  21. # ### Memory Efficiency
  22. # - **Object reuse**: Formatter instances are reused across log calls
  23. # - **Lazy evaluation**: Only processes data that will be included in output
  24. # - **Efficient buffering**: Optimal buffer sizes for JSON generation
  25. # - **Garbage collection friendly**: Minimal object allocation reduces GC pressure
  26. #
  27. # ### Integration Benefits
  28. # - **LogStruct compatibility**: Native support for typed log structures
  29. # - **Filter preservation**: Maintains all LogStruct filtering capabilities
  30. # - **Scrubbing integration**: Seamless sensitive data scrubbing
  31. # - **Error handling**: Robust handling of serialization errors
  32. #
  33. # ## Feature Preservation:
  34. # This formatter maintains full compatibility with LogStruct's features:
  35. # - Sensitive data filtering (passwords, tokens, etc.)
  36. # - Recursive object scrubbing and processing
  37. # - Type-safe log structure handling
  38. # - Custom field transformations
  39. # - Metadata preservation and enrichment
  40. #
  41. # ## JSON Output Structure:
  42. # The formatter produces consistent, parseable JSON that includes:
  43. # - Standard log fields (timestamp, level, message, logger name)
  44. # - LogStruct-specific fields (source, event, context)
  45. # - SemanticLogger metadata (process ID, thread ID, tags)
  46. # - Application-specific payload data
  47. #
  48. # This combination provides the performance benefits of SemanticLogger with
  49. # the structured data benefits of LogStruct, resulting in faster, more
  50. # reliable logging for high-traffic applications.
  51. 2 class Formatter < ::SemanticLogger::Formatters::Json
  52. 2 extend T::Sig
  53. 4 sig { void }
  54. 2 def initialize
  55. 42 super
  56. 42 @logstruct_formatter = T.let(LogStruct::Formatter.new, LogStruct::Formatter)
  57. end
  58. 4 sig { params(log: ::SemanticLogger::Log, logger: T.untyped).returns(String) }
  59. 2 def call(log, logger)
  60. # Handle LogStruct types specially - they get wrapped in payload hash by SemanticLogger
  61. 847 json = if log.payload.is_a?(Hash) && log.payload[:payload].is_a?(LogStruct::Log::Interfaces::CommonFields)
  62. # Use our formatter to process LogStruct types
  63. 53 @logstruct_formatter.call(log.level, log.time, log.name, log.payload[:payload])
  64. 794 elsif log.payload.is_a?(LogStruct::Log::Interfaces::CommonFields)
  65. # Direct LogStruct (fallback case)
  66. @logstruct_formatter.call(log.level, log.time, log.name, log.payload)
  67. 794 elsif log.payload.is_a?(Hash) && log.payload[:payload].is_a?(T::Struct)
  68. # T::Struct wrapped in payload hash
  69. 2 @logstruct_formatter.call(log.level, log.time, log.name, log.payload[:payload])
  70. 792 elsif log.payload.is_a?(Hash) || log.payload.is_a?(T::Struct)
  71. # Process hashes and T::Structs through our formatter
  72. 7 @logstruct_formatter.call(log.level, log.time, log.name, log.payload)
  73. else
  74. # For plain messages, create a Plain log entry
  75. 785 message_data = log.payload || log.message
  76. 785 plain_log = ::LogStruct::Log::Plain.new(
  77. message: message_data,
  78. timestamp: log.time
  79. )
  80. 785 @logstruct_formatter.call(log.level, log.time, log.name, plain_log)
  81. end
  82. # SemanticLogger appenders typically add their own newline. Avoid double newlines by stripping ours.
  83. 847 json.end_with?("\n") ? json.chomp : json
  84. end
  85. 2 private
  86. 2 sig { returns(LogStruct::Formatter) }
  87. 2 attr_reader :logstruct_formatter
  88. end
  89. end
  90. end

lib/log_struct/semantic_logger/logger.rb

83.33% lines covered

54 relevant lines. 45 lines covered and 9 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "semantic_logger"
  4. 2 require_relative "concerns/log_methods"
  5. 2 module LogStruct
  6. 2 module SemanticLogger
  7. # High-Performance Logger with LogStruct Integration
  8. #
  9. # This logger extends SemanticLogger::Logger to provide optimal logging performance
  10. # while seamlessly integrating with LogStruct's typed logging system.
  11. #
  12. # ## Key Benefits Over Rails.logger:
  13. #
  14. # ### Performance
  15. # - **10-100x faster** than Rails' default logger for high-volume applications
  16. # - **Non-blocking I/O**: Uses background threads for actual log writes
  17. # - **Minimal memory allocation**: Efficient object reuse and zero-copy operations
  18. # - **Batched writes**: Reduces system calls by batching multiple log entries
  19. #
  20. # ### Reliability
  21. # - **Thread-safe operations**: Safe for use in multi-threaded environments
  22. # - **Error resilience**: Logger failures don't crash your application
  23. # - **Graceful fallbacks**: Continues operating even if appenders fail
  24. #
  25. # ### Features
  26. # - **Structured logging**: Native support for LogStruct types and hashes
  27. # - **Rich metadata**: Automatic inclusion of process ID, thread ID, timestamps
  28. # - **Tagged context**: Hierarchical tagging for request/job tracking
  29. # - **Multiple destinations**: Simultaneously log to files, STDOUT, cloud services
  30. #
  31. # ### Development Experience
  32. # - **Colorized output**: Beautiful ANSI-colored logs in development
  33. # - **Detailed timing**: Built-in measurement of log processing time
  34. # - **Context preservation**: Maintains Rails.logger compatibility
  35. #
  36. # ## Usage Examples
  37. #
  38. # The logger automatically handles LogStruct types, hashes, and plain messages:
  39. #
  40. # ```ruby
  41. # logger = LogStruct::SemanticLogger::Logger.new("MyApp")
  42. #
  43. # # LogStruct typed logging (optimal performance)
  44. # log_entry = LogStruct::Log::Plain.new(
  45. # message: "User authenticated",
  46. # source: LogStruct::Source::App,
  47. # event: LogStruct::Event::Security
  48. # )
  49. # logger.info(log_entry)
  50. #
  51. # # Hash logging (automatically structured)
  52. # logger.info({
  53. # action: "user_login",
  54. # user_id: 123,
  55. # ip_address: "192.168.1.1"
  56. # })
  57. #
  58. # # Plain string logging (backward compatibility)
  59. # logger.info("User logged in successfully")
  60. # ```
  61. #
  62. # The logger is a drop-in replacement for Rails.logger and maintains full
  63. # API compatibility while providing significantly enhanced performance.
  64. 2 class Logger < ::SemanticLogger::Logger
  65. 2 extend T::Sig
  66. 4 sig { params(name: T.any(String, Symbol, Module, T::Class[T.anything]), level: T.nilable(Symbol), filter: T.untyped).void }
  67. 2 def initialize(name = "Application", level: nil, filter: nil)
  68. # SemanticLogger::Logger expects positional arguments, not named arguments
  69. 42 super(name, level, filter)
  70. # T.untyped because users can pass any logger: ::Logger, ActiveSupport::Logger,
  71. # custom loggers (FakeLogger in tests), or third-party loggers
  72. 42 @broadcasts = T.let([], T::Array[T.untyped])
  73. # ActiveJob expects logger.formatter to exist and respond to current_tags
  74. 42 @formatter = T.let(FormatterProxy.new, FormatterProxy)
  75. end
  76. # ActiveSupport::BroadcastLogger compatibility
  77. # These methods allow Rails.logger to broadcast to multiple loggers
  78. 4 sig { returns(T::Array[T.untyped]) }
  79. 2 attr_reader :broadcasts
  80. # ActiveJob compatibility - expects logger.formatter.current_tags
  81. 4 sig { returns(FormatterProxy) }
  82. 2 attr_reader :formatter
  83. # T.untyped for logger param because we accept any logger-like object:
  84. # ::Logger, ActiveSupport::Logger, test doubles, etc.
  85. 3 sig { params(logger: T.untyped).returns(T.untyped) }
  86. 2 def broadcast_to(logger)
  87. 6 @broadcasts << logger
  88. 6 logger
  89. end
  90. 3 sig { params(logger: T.untyped).void }
  91. 2 def stop_broadcasting_to(logger)
  92. 1 @broadcasts.delete(logger)
  93. end
  94. 2 include Concerns::LogMethods
  95. # Support for tagged logging
  96. 3 sig { params(tags: T.untyped, block: T.proc.returns(T.untyped)).returns(T.untyped) }
  97. 2 def tagged(*tags, &block)
  98. # Convert tags to array and pass individually to avoid splat issues
  99. 1 tag_array = tags.flatten
  100. 1 if tag_array.empty?
  101. super(&block)
  102. else
  103. 1 super(*T.unsafe(tag_array), &block)
  104. end
  105. end
  106. # Ensure compatibility with Rails.logger interface
  107. 2 sig { returns(T::Array[T.any(String, Symbol)]) }
  108. 2 def current_tags
  109. ::SemanticLogger.tags
  110. end
  111. 2 sig { void }
  112. 2 def clear_tags!
  113. # SemanticLogger doesn't have clear_tags!, use pop_tags instead
  114. count = ::SemanticLogger.tags.length
  115. ::SemanticLogger.pop_tags(count) if count > 0
  116. end
  117. 2 sig { params(tags: T.untyped).returns(T::Array[T.untyped]) }
  118. 2 def push_tags(*tags)
  119. flat = tags.flatten.compact
  120. flat.each { |tag| ::SemanticLogger.push_tags(tag) }
  121. flat
  122. end
  123. 2 sig { params(count: Integer).void }
  124. 2 def pop_tags(count = 1)
  125. ::SemanticLogger.pop_tags(count)
  126. end
  127. # Support for << operator (used by RailsLogSplitter)
  128. 3 sig { params(msg: String).returns(T.self_type) }
  129. 2 def <<(msg)
  130. 1 info(msg)
  131. 2 @broadcasts.each { |logger| logger << msg if logger.respond_to?(:<<) }
  132. 1 self
  133. end
  134. end
  135. # Proxy object to provide ActiveJob-compatible formatter interface
  136. 2 class FormatterProxy
  137. 2 extend T::Sig
  138. 2 sig { returns(T::Array[T.any(String, Symbol)]) }
  139. 2 def current_tags
  140. Thread.current[:activesupport_tagged_logging_tags] || []
  141. end
  142. end
  143. end
  144. end

lib/log_struct/semantic_logger/setup.rb

80.95% lines covered

63 relevant lines. 51 lines covered and 12 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 module LogStruct
  8. # SemanticLogger Integration
  9. #
  10. # LogStruct uses SemanticLogger as its core logging engine, providing significant
  11. # performance and functionality benefits over Rails' default logger:
  12. #
  13. # ## Performance Benefits
  14. # - **Asynchronous logging**: Logs are written in a background thread, eliminating
  15. # I/O blocking in your main application threads
  16. # - **High throughput**: Can handle 100,000+ log entries per second
  17. # - **Memory efficient**: Structured data processing with minimal allocations
  18. # - **Zero-copy serialization**: Direct JSON generation without intermediate objects
  19. #
  20. # ## Reliability Benefits
  21. # - **Thread-safe**: All operations are thread-safe by design
  22. # - **Graceful degradation**: Continues logging even if appenders fail
  23. # - **Error isolation**: Logging errors don't crash your application
  24. # - **Buffered writes**: Reduces disk I/O with intelligent batching
  25. #
  26. # ## Feature Benefits
  27. # - **Multiple appenders**: Log to files, STDOUT, databases, cloud services simultaneously
  28. # - **Structured metadata**: Rich context including process ID, thread ID, tags, and more
  29. # - **Log filtering**: Runtime filtering by logger name, level, or custom rules
  30. # - **Formatters**: Pluggable output formatting (JSON, colorized, custom)
  31. # - **Metrics integration**: Built-in performance metrics and timing data
  32. #
  33. # ## Development Experience
  34. # - **Colorized output**: Beautiful, readable logs in development with ANSI colors
  35. # - **Tagged logging**: Hierarchical context tracking (requests, jobs, etc.)
  36. # - **Debugging tools**: Detailed timing and memory usage information
  37. # - **Hot reloading**: Configuration changes without application restart
  38. #
  39. # ## Production Benefits
  40. # - **Log rotation**: Automatic file rotation with size/time-based policies
  41. # - **Compression**: Automatic log compression to save disk space
  42. # - **Cloud integration**: Direct integration with CloudWatch, Splunk, etc.
  43. # - **Alerting**: Built-in support for error alerting and monitoring
  44. #
  45. # ## LogStruct Specific Enhancements
  46. # - **Type safety**: Full Sorbet type annotations for compile-time error detection
  47. # - **Structured data**: Native support for LogStruct's typed log structures
  48. # - **Filtering integration**: Seamless integration with LogStruct's data filters
  49. # - **Error handling**: Enhanced error reporting with full stack traces and context
  50. #
  51. # SemanticLogger is a production-grade logging framework used by companies processing
  52. # millions of requests per day. It provides the performance and reliability needed
  53. # for high-traffic Rails applications while maintaining an elegant developer experience.
  54. 2 module SemanticLogger
  55. # Handles setup and configuration of SemanticLogger for Rails applications
  56. #
  57. # This module provides the core integration between LogStruct and SemanticLogger,
  58. # configuring appenders, formatters, and logger replacement to provide optimal
  59. # logging performance while maintaining full compatibility with Rails conventions.
  60. 2 module Setup
  61. 2 extend T::Sig
  62. # Configures SemanticLogger as the primary logging engine for the Rails application
  63. #
  64. # This method replaces Rails' default logger with SemanticLogger, providing:
  65. # - **10-100x performance improvement** for high-volume logging
  66. # - **Non-blocking I/O** through background thread processing
  67. # - **Enhanced reliability** with graceful error handling
  68. # - **Multiple output destinations** (files, STDOUT, cloud services)
  69. # - **Structured metadata** including process/thread IDs and timing
  70. #
  71. # The configuration automatically:
  72. # - Determines optimal log levels based on environment
  73. # - Sets up appropriate appenders (console, file, etc.)
  74. # - Enables colorized output in development
  75. # - Replaces Rails.logger and component loggers
  76. # - Preserves full Rails.logger API compatibility
  77. #
  78. # @param app [Rails::Application] The Rails application instance
  79. 4 sig { params(app: T.untyped).void }
  80. 2 def self.configure_semantic_logger(app)
  81. # Set SemanticLogger configuration
  82. 2 ::SemanticLogger.application = Rails.application.class.module_parent_name
  83. 2 ::SemanticLogger.environment = Rails.env
  84. # Determine log level from Rails config
  85. 2 log_level = determine_log_level(app)
  86. 2 ::SemanticLogger.default_level = log_level
  87. # Clear existing appenders
  88. 2 ::SemanticLogger.clear_appenders!
  89. # Add appropriate appenders based on environment
  90. 2 add_appenders(app)
  91. # Replace Rails.logger with SemanticLogger
  92. 2 replace_rails_logger(app)
  93. end
  94. 4 sig { params(app: T.untyped).returns(Symbol) }
  95. 2 def self.determine_log_level(app)
  96. 2 if app.config.log_level
  97. 2 app.config.log_level
  98. elsif Rails.env.production?
  99. :info
  100. elsif Rails.env.test?
  101. :debug
  102. else
  103. :debug
  104. end
  105. end
  106. 4 sig { params(app: T.untyped).void }
  107. 2 def self.add_appenders(app)
  108. 2 config = LogStruct.config
  109. # Determine output destination
  110. 2 io = determine_output(app)
  111. 2 if Rails.env.development?
  112. if config.prefer_json_in_development
  113. # Default to production-style JSON in development when enabled
  114. ::SemanticLogger.add_appender(
  115. io: io,
  116. formatter: LogStruct::SemanticLogger::Formatter.new,
  117. filter: determine_filter
  118. )
  119. elsif config.enable_color_output
  120. # Opt-in colorful human formatter in development
  121. ::SemanticLogger.add_appender(
  122. io: io,
  123. formatter: LogStruct::SemanticLogger::ColorFormatter.new(
  124. color_map: config.color_map
  125. ),
  126. filter: determine_filter
  127. )
  128. else
  129. ::SemanticLogger.add_appender(
  130. io: io,
  131. formatter: LogStruct::SemanticLogger::Formatter.new,
  132. filter: determine_filter
  133. )
  134. end
  135. else
  136. # Use our custom JSON formatter in non-development environments
  137. 2 ::SemanticLogger.add_appender(
  138. io: io,
  139. formatter: LogStruct::SemanticLogger::Formatter.new,
  140. filter: determine_filter
  141. )
  142. end
  143. # Add file appender if Rails has a log path configured (normal Rails behavior)
  144. 2 if app.config.paths["log"].first
  145. 2 ::SemanticLogger.add_appender(
  146. file_name: app.config.paths["log"].first,
  147. formatter: LogStruct::SemanticLogger::Formatter.new,
  148. filter: determine_filter
  149. )
  150. end
  151. end
  152. 4 sig { params(app: T.untyped).returns(T.untyped) }
  153. 2 def self.determine_output(app)
  154. # Always honor explicit STDOUT directive
  155. 2 return $stdout if ENV["RAILS_LOG_TO_STDOUT"].present?
  156. 2 if Rails.env.test?
  157. # Use StringIO in test to keep stdout clean
  158. 2 StringIO.new
  159. else
  160. # Use STDOUT for app logs in dev/production
  161. $stdout
  162. end
  163. end
  164. 4 sig { returns(T.nilable(Regexp)) }
  165. 2 def self.determine_filter
  166. # Filter out noisy loggers if configured
  167. 4 config = LogStruct.config
  168. 4 return nil unless config.filter_noisy_loggers
  169. # Common noisy loggers to filter
  170. /\A(ActionView|ActionController::RoutingError|ActiveRecord::SchemaMigration)/
  171. end
  172. # Replaces Rails.logger and all component loggers with LogStruct's SemanticLogger
  173. #
  174. # This method provides seamless integration by replacing the default Rails logger
  175. # throughout the entire Rails stack, ensuring all logging flows through the
  176. # high-performance SemanticLogger system.
  177. #
  178. # ## Benefits of Complete Logger Replacement:
  179. # - **Consistent performance**: All Rails components benefit from SemanticLogger speed
  180. # - **Unified formatting**: All logs use the same structured JSON format
  181. # - **Centralized configuration**: Single point of control for all logging
  182. # - **Complete compatibility**: Maintains all Rails.logger API contracts
  183. #
  184. # ## Components Updated:
  185. # - Rails.logger (framework core)
  186. # - ActiveRecord::Base.logger (database queries)
  187. # - ActionController::Base.logger (request processing)
  188. # - ActionMailer::Base.logger (email delivery)
  189. # - ActiveJob::Base.logger (background jobs)
  190. # - ActionView::Base.logger (template rendering)
  191. # - ActionCable.server.config.logger (WebSocket connections)
  192. #
  193. # After replacement, all Rails logging maintains API compatibility while gaining
  194. # SemanticLogger's performance, reliability, and feature benefits.
  195. #
  196. # @param app [Rails::Application] The Rails application instance
  197. 4 sig { params(app: T.untyped).void }
  198. 2 def self.replace_rails_logger(app)
  199. # Create new SemanticLogger instance
  200. 2 logger = LogStruct::SemanticLogger::Logger.new("Rails")
  201. # Replace Rails.logger
  202. 2 Rails.logger = logger
  203. # Also replace various component loggers
  204. 2 ActiveRecord::Base.logger = logger if defined?(ActiveRecord::Base)
  205. 2 ActionController::Base.logger = logger if defined?(ActionController::Base)
  206. 2 if defined?(ActionMailer::Base)
  207. 2 ActionMailer::Base.logger = logger
  208. # Ensure ActionMailer.logger is also set (it might be accessed directly)
  209. 2 T.unsafe(::ActionMailer).logger = logger if T.unsafe(::ActionMailer).respond_to?(:logger=)
  210. end
  211. 2 ActiveJob::Base.logger = logger if defined?(ActiveJob::Base)
  212. 2 ActionView::Base.logger = logger if defined?(ActionView::Base)
  213. 2 ActionCable.server.config.logger = logger if defined?(ActionCable)
  214. # Store reference in app config
  215. 2 app.config.logger = logger
  216. end
  217. end
  218. end
  219. end

lib/log_struct/shared/add_request_fields.rb

64.71% lines covered

17 relevant lines. 11 lines covered and 6 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. hash[LogField::RequestId.serialize] = request_id if request_id
  20. end
  21. end
  22. end
  23. end
  24. 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_fields.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_relative "../../enums/source"
  4. 2 require_relative "../../enums/event"
  5. 2 require_relative "../../enums/level"
  6. 2 module LogStruct
  7. 2 module Log
  8. 2 module Interfaces
  9. 2 module CommonFields
  10. 2 extend T::Sig
  11. 2 extend T::Helpers
  12. 2 interface!
  13. 2 sig { abstract.returns(Source) }
  14. 2 def source
  15. end
  16. 2 sig { abstract.returns(Event) }
  17. 2 def event
  18. end
  19. 2 sig { abstract.returns(Level) }
  20. 2 def level
  21. end
  22. 2 sig { abstract.returns(Time) }
  23. 2 def timestamp
  24. end
  25. 2 sig { abstract.params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
  26. 2 def serialize(strict = true)
  27. end
  28. end
  29. end
  30. end
  31. end

lib/log_struct/shared/interfaces/public_common_fields.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 PublicCommonFields
  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/request_fields.rb

100.0% lines covered

19 relevant lines. 19 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. 2 sig { abstract.returns(T.nilable(String)) }
  26. 2 def request_id
  27. end
  28. end
  29. end
  30. end
  31. end

lib/log_struct/shared/merge_additional_data_fields.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 require_relative "interfaces/additional_data_field"
  4. 2 module LogStruct
  5. 2 module Log
  6. 2 module Shared
  7. 2 module MergeAdditionalDataFields
  8. 2 extend T::Sig
  9. 2 extend T::Helpers
  10. 2 requires_ancestor { T::Struct }
  11. 2 requires_ancestor { Interfaces::AdditionalDataField }
  12. 4 sig { params(hash: T::Hash[Symbol, T.untyped]).void }
  13. 2 def merge_additional_data_fields(hash)
  14. 840 ad = additional_data
  15. 840 return unless ad
  16. 19 ad.each do |key, value|
  17. 21 hash[key.to_sym] = value
  18. end
  19. end
  20. end
  21. end
  22. end
  23. end

lib/log_struct/shared/serialize_common.rb

100.0% lines covered

31 relevant lines. 31 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. 883 out = serialize_common(strict)
  17. # Merge event/base fields from the struct-specific hash
  18. 883 kernel_self = T.cast(self, Kernel)
  19. 883 field_hash = T.cast(kernel_self.public_send(:to_h), T::Hash[LogStruct::LogField, T.untyped])
  20. 883 field_hash.each do |log_field, value|
  21. 1131 next if value.nil?
  22. 1131 key = log_field.serialize
  23. # Limit backtrace to first 5 lines
  24. 1131 if key == :backtrace && value.is_a?(Array)
  25. 4 value = value.first(5)
  26. end
  27. 1131 out[key] = value.is_a?(::Time) ? value.iso8601 : value
  28. end
  29. # Merge any additional_data at top level if available
  30. 883 if kernel_self.respond_to?(:merge_additional_data_fields)
  31. # merge_additional_data_fields expects symbol keys
  32. 837 merge_target = T.cast(self, LogStruct::Log::Shared::MergeAdditionalDataFields)
  33. 837 merge_target.merge_additional_data_fields(out)
  34. end
  35. 883 out
  36. end
  37. 4 sig { params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
  38. 2 def serialize_common(strict = true)
  39. {
  40. 883 LogField::Source.serialize => source.serialize.to_s,
  41. LogField::Event.serialize => event.serialize.to_s,
  42. LogField::Level.serialize => level.serialize.to_s,
  43. LogField::Timestamp.serialize => timestamp.iso8601(3)
  44. }
  45. end
  46. 3 sig { params(options: T.untyped).returns(T::Hash[String, T.untyped]) }
  47. 2 def as_json(options = nil)
  48. 16 serialize.transform_keys(&:to_s)
  49. end
  50. end
  51. end
  52. end
  53. 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. 3 unless respond_to?(:source) && respond_to?(:event)
  16. raise ArgumentError, "Public log struct must define #source and #event"
  17. end
  18. 3 src_val = public_send(:source)
  19. 3 evt_val = public_send(:event)
  20. 3 src = src_val.respond_to?(:serialize) ? src_val.public_send(:serialize).to_s : src_val.to_s
  21. 3 evt = evt_val.respond_to?(:serialize) ? evt_val.public_send(:serialize).to_s : evt_val.to_s
  22. 3 lvl = level.serialize.to_s
  23. 3 ts = timestamp.iso8601(3)
  24. {
  25. 3 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

83.33% lines covered

12 relevant lines. 10 lines covered and 2 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. 2 sig { params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
  10. 2 def serialize(strict = true)
  11. 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

100.0% lines covered

39 relevant lines. 39 lines covered and 0 lines missed.
    
  1. # typed: strict
  2. # frozen_string_literal: true
  3. 2 require "digest"
  4. 2 module LogStruct
  5. # StringScrubber is inspired by logstop by @ankane: https://github.com/ankane/logstop
  6. # Enhancements:
  7. # - Shows which type of data was filtered
  8. # - Includes an SHA256 hash with filtered emails for request tracing
  9. # - Uses configuration options from LogStruct.config
  10. 2 module StringScrubber
  11. 2 class << self
  12. 2 extend T::Sig
  13. # Also supports URL-encoded URLs like https%3A%2F%2Fuser%3Asecret%40example.com
  14. # cspell:ignore Fuser Asecret
  15. 2 URL_PASSWORD_REGEX = /((?:\/\/|%2F%2F)[^\s\/]+(?::|%3A))[^\s\/]+(@|%40)/
  16. 2 URL_PASSWORD_REPLACEMENT = '\1[PASSWORD]\2'
  17. 2 EMAIL_REGEX = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i
  18. 2 CREDIT_CARD_REGEX_SHORT = /\b[3456]\d{15}\b/
  19. 2 CREDIT_CARD_REGEX_DELIMITERS = /\b[3456]\d{3}[\s-]\d{4}[\s-]\d{4}[\s-]\d{4}\b/
  20. 2 CREDIT_CARD_REPLACEMENT = "[CREDIT_CARD]"
  21. 2 PHONE_REGEX = /\b\d{3}[\s-]\d{3}[\s-]\d{4}\b/
  22. 2 PHONE_REPLACEMENT = "[PHONE]"
  23. 2 SSN_REGEX = /\b\d{3}[\s-]\d{2}[\s-]\d{4}\b/
  24. 2 SSN_REPLACEMENT = "[SSN]"
  25. 2 IP_REGEX = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/
  26. 2 IP_REPLACEMENT = "[IP]"
  27. 2 MAC_REGEX = /\b[0-9a-f]{2}(:[0-9a-f]{2}){5}\b/i
  28. 2 MAC_REPLACEMENT = "[MAC]"
  29. # Scrub sensitive information from a string
  30. 4 sig { params(string: String).returns(String) }
  31. 2 def scrub(string)
  32. 4426 return string if string.empty?
  33. 4426 string = string.to_s.dup
  34. 4426 config = LogStruct.config.filters
  35. # Passwords in URLs
  36. 4426 string.gsub!(URL_PASSWORD_REGEX, URL_PASSWORD_REPLACEMENT) if config.url_passwords
  37. # Emails
  38. 4426 if config.email_addresses
  39. 4425 string.gsub!(EMAIL_REGEX) do |email|
  40. 11 email_hash = HashUtils.hash_value(email)
  41. 11 "[EMAIL:#{email_hash}]"
  42. end
  43. end
  44. # Credit card numbers
  45. 4426 if config.credit_card_numbers
  46. 4425 string.gsub!(CREDIT_CARD_REGEX_SHORT, CREDIT_CARD_REPLACEMENT)
  47. 4425 string.gsub!(CREDIT_CARD_REGEX_DELIMITERS, CREDIT_CARD_REPLACEMENT)
  48. end
  49. # Phone numbers
  50. 4426 string.gsub!(PHONE_REGEX, PHONE_REPLACEMENT) if config.phone_numbers
  51. # SSNs
  52. 4426 string.gsub!(SSN_REGEX, SSN_REPLACEMENT) if config.ssns
  53. # IPs
  54. 4426 string.gsub!(IP_REGEX, IP_REPLACEMENT) if config.ip_addresses
  55. # MAC addresses
  56. 4426 string.gsub!(MAC_REGEX, MAC_REPLACEMENT) if config.mac_addresses
  57. # Custom scrubber
  58. 4426 custom_scrubber = LogStruct.config.string_scrubbing_handler
  59. 4426 string = custom_scrubber.call(string) if !custom_scrubber.nil?
  60. 4426 string
  61. end
  62. end
  63. end
  64. 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/logging_controller.rb

70.21% lines covered

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

8 relevant lines. 8 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. 1 has_one_attached :file
  6. 2 sig { params(filename: String, content: String).returns(Document) }
  7. 1 def self.create_with_file(filename:, content:)
  8. 4 document = T.let(create!, Document)
  9. 4 document.file.attach(
  10. io: StringIO.new(content),
  11. filename: filename,
  12. content_type: "text/plain"
  13. )
  14. 4 document
  15. end
  16. end

rails_test_app/logstruct_test_app/app/models/user.rb

72.73% lines covered

11 relevant lines. 8 lines covered and 3 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. 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/config/application.rb

100.0% lines covered

12 relevant lines. 12 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.0
  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. # Use test adapter for ActiveJob in all environments for testing
  21. 1 config.active_job.queue_adapter = :test
  22. # Force all environments to log to STDOUT so development behaves like test/production
  23. # This mirrors how many platforms and 12-factor apps expect logs to be emitted.
  24. 1 config.log_level = :debug
  25. 1 stdout_logger = ActiveSupport::Logger.new($stdout)
  26. 1 stdout_logger.formatter = config.log_formatter
  27. 1 config.logger = ActiveSupport::TaggedLogging.new(stdout_logger)
  28. end
  29. 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

100.0% lines covered

22 relevant lines. 22 lines covered and 0 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. # Specify which environments are considered local/development
  8. 1 config.local_environments = [:development, :test]
  9. # Configure integrations
  10. 1 config.integrations.enable_lograge = true
  11. 1 config.integrations.enable_actionmailer = true
  12. 1 config.integrations.enable_activejob = true
  13. 1 config.integrations.enable_rack_error_handler = true
  14. 1 config.integrations.enable_sidekiq = !!defined?(Sidekiq)
  15. 1 config.integrations.enable_shrine = !!defined?(Shrine)
  16. 1 config.integrations.enable_carrierwave = !!defined?(CarrierWave)
  17. 1 config.integrations.enable_activestorage = true
  18. # Configure string scrubbing filters
  19. 1 config.filters.email_addresses = true
  20. 1 config.filters.url_passwords = true
  21. 1 config.filters.credit_card_numbers = true
  22. 1 config.filters.phone_numbers = true
  23. 1 config.filters.ssns = true
  24. 1 config.filters.ip_addresses = true
  25. 1 config.filters.mac_addresses = true
  26. # Configure error handling modes
  27. 1 config.error_handling_modes.logstruct_errors = LogStruct::ErrorHandlingMode::Log
  28. 1 config.error_handling_modes.security_errors = LogStruct::ErrorHandlingMode::Report
  29. 1 config.error_handling_modes.standard_errors = LogStruct::ErrorHandlingMode::LogProduction
  30. end

rails_test_app/logstruct_test_app/config/routes.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 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. # Healthcheck route
  14. 3 get "/health", to: proc { [200, {}, ["OK"]] }
  15. end

rails_test_app/logstruct_test_app/test/integration/action_mailer_id_mapping_test.rb

100.0% lines covered

44 relevant lines. 44 lines covered and 0 lines missed.
    
  1. # typed: true
  2. # frozen_string_literal: true
  3. 1 require "test_helper"
  4. 1 class ActionMailerIdMappingTest < ActiveSupport::TestCase
  5. 1 setup do
  6. 2 @original_mapping = LogStruct.config.integrations.actionmailer_id_mapping
  7. # Use StringIO to capture log output
  8. 2 @log_output = StringIO.new
  9. 2 @original_logger = Rails.logger
  10. # Create a new logger with our StringIO and LogStruct's formatter
  11. 2 logger = Logger.new(@log_output)
  12. 2 logger.formatter = LogStruct::Formatter.new
  13. 2 Rails.logger = logger
  14. end
  15. 1 teardown do
  16. 2 LogStruct.config.integrations.actionmailer_id_mapping = @original_mapping
  17. 2 Rails.logger = @original_logger
  18. end
  19. # Helper method to parse log entries
  20. 1 def find_log_entries(event_type)
  21. 2 @log_output.rewind
  22. 2 logs = []
  23. 2 @log_output.each_line do |line|
  24. 4 if line =~ /(\{.+\})/
  25. 4 json = JSON.parse($1)
  26. 4 logs << json if json["src"] == "mailer" && json["evt"] == event_type
  27. end
  28. rescue JSON::ParserError
  29. # Skip lines that don't contain valid JSON
  30. end
  31. 2 logs
  32. end
  33. 1 test "actionmailer_id_mapping extracts configured instance variables as IDs in additional_data" do
  34. # Clear the log buffer before the test
  35. 1 @log_output.truncate(0)
  36. 1 @log_output.rewind
  37. # Configure default ID mapping
  38. 1 LogStruct.config.integrations.actionmailer_id_mapping = {
  39. account: :account_id,
  40. user: :user_id
  41. }
  42. # Create test objects
  43. 1 account = Struct.new(:id).new(123)
  44. 1 user = Struct.new(:id).new(456)
  45. # Deliver email
  46. 1 TestMailer.test_email_with_ids(account, user).deliver_now
  47. # Find delivery logs in the captured output
  48. 1 delivery_logs = find_log_entries("delivered")
  49. 1 assert_not_empty delivery_logs, "Expected delivery logs to be generated"
  50. 1 delivered_log = delivery_logs.first
  51. # Check that account_id and user_id are in the log
  52. 1 assert_equal 123, delivered_log["account_id"]
  53. 1 assert_equal 456, delivered_log["user_id"]
  54. end
  55. 1 test "actionmailer_id_mapping uses custom field names" do
  56. # Clear the log buffer before the test
  57. 1 @log_output.truncate(0)
  58. 1 @log_output.rewind
  59. # Configure custom ID mapping
  60. 1 LogStruct.config.integrations.actionmailer_id_mapping = {
  61. organization: :org_id
  62. }
  63. # Create test object
  64. 1 organization = Struct.new(:id).new(789)
  65. # Deliver email
  66. 1 TestMailer.test_email_with_organization(organization).deliver_now
  67. # Find delivery logs in the captured output
  68. 1 delivery_logs = find_log_entries("delivered")
  69. 1 assert_not_empty delivery_logs, "Expected delivery logs to be generated"
  70. 1 delivered_log = delivery_logs.first
  71. # Check that org_id is in the log
  72. 1 assert_equal 789, delivered_log["org_id"]
  73. # Should not have account_id or user_id
  74. 1 assert_nil delivered_log["account_id"]
  75. 1 assert_nil delivered_log["user_id"]
  76. end
  77. end

rails_test_app/logstruct_test_app/test/integration/active_storage_test.rb

98.86% lines covered

88 relevant lines. 87 lines covered and 1 lines missed.
    
  1. # typed: true
  2. # frozen_string_literal: true
  3. 1 require "test_helper"
  4. 1 class ActiveStorageTest < ActiveSupport::TestCase
  5. 1 setup do
  6. # Use StringIO to capture log output
  7. 5 @log_output = StringIO.new
  8. 5 @original_logger = Rails.logger
  9. # Create a new logger with our StringIO and LogStruct's formatter
  10. 5 logger = Logger.new(@log_output)
  11. 5 logger.formatter = LogStruct::Formatter.new
  12. 5 Rails.logger = logger
  13. end
  14. 1 teardown do
  15. # Restore the original logger
  16. 5 Rails.logger = @original_logger
  17. end
  18. # Helper method to parse log entries
  19. 1 def find_log_entries(event_type)
  20. # Reset the StringIO position to the beginning
  21. 5 @log_output.rewind
  22. # Parse the log contents looking for JSON data
  23. 5 logs = []
  24. 5 @log_output.each_line do |line|
  25. # Log lines might have timestamps or other text before the JSON
  26. 5 if line =~ /(\{.+\})/
  27. 5 json = JSON.parse($1)
  28. # Only include active storage logs with the specified event
  29. 5 logs << json if json["src"] == "storage" && json["evt"] == event_type
  30. end
  31. rescue JSON::ParserError
  32. # Skip lines that don't contain valid JSON
  33. end
  34. 5 logs
  35. end
  36. 1 test "logs are created when uploading a file" do
  37. # Clear the log buffer before the test
  38. 1 @log_output.truncate(0)
  39. 1 @log_output.rewind
  40. # Create a document with an attached file, which should trigger upload
  41. 1 Document.create_with_file(
  42. filename: "test_file.txt",
  43. content: "This is test content for Active Storage"
  44. )
  45. # Give some time for the async events to process
  46. 1 sleep(0.2)
  47. # Find upload logs in the captured output
  48. 1 upload_logs = find_log_entries("upload")
  49. 1 assert_not_empty upload_logs, "Expected upload logs to be generated"
  50. 1 upload_log = upload_logs.first
  51. 1 assert_equal "storage", upload_log["src"]
  52. 1 assert_equal "upload", upload_log["evt"]
  53. 1 assert_equal "Disk", upload_log["storage"]
  54. 1 assert_not_nil upload_log["file_id"]
  55. 1 assert_not_nil upload_log["checksum"]
  56. 1 assert_not_nil upload_log["duration_ms"]
  57. end
  58. 1 test "logs are created when downloading a file" do
  59. # Create a document with a file for testing
  60. 1 document = Document.create_with_file(
  61. filename: "download_test.txt",
  62. content: "This is content to download"
  63. )
  64. # Clear the log buffer before the test
  65. 1 @log_output.truncate(0)
  66. 1 @log_output.rewind
  67. # Download the file
  68. 1 document.file.download
  69. # Give some time for the async events to process
  70. 1 sleep(0.2)
  71. # Find download logs in the captured output
  72. 1 download_logs = find_log_entries("download")
  73. 1 assert_not_empty download_logs, "Expected download logs to be generated"
  74. 1 download_log = download_logs.first
  75. 1 assert_equal "storage", download_log["src"]
  76. 1 assert_equal "download", download_log["evt"]
  77. 1 assert_equal "Disk", download_log["storage"]
  78. 1 assert_not_nil download_log["file_id"]
  79. 1 assert_not_nil download_log["duration_ms"]
  80. end
  81. 1 test "logs are created when checking if a file exists" do
  82. # Create a document with a file for testing
  83. 1 document = Document.create_with_file(
  84. filename: "exist_test.txt",
  85. content: "This is content to check existence"
  86. )
  87. # Clear the log buffer before the test
  88. 1 @log_output.truncate(0)
  89. 1 @log_output.rewind
  90. # Check if file exists - we need to hit the storage service directly to trigger the exist event
  91. # In ActiveStorage, we need to directly check through the storage service
  92. 1 storage = ActiveStorage::Blob.service
  93. 1 storage.exist?(document.file.key)
  94. # Give some time for the async events to process
  95. 1 sleep(0.2)
  96. # Find existence check logs in the captured output
  97. 1 exist_logs = find_log_entries("exist")
  98. 1 assert_not_empty exist_logs, "Expected existence check logs to be generated"
  99. 1 exist_log = exist_logs.first
  100. 1 assert_equal "storage", exist_log["src"]
  101. 1 assert_equal "exist", exist_log["evt"]
  102. 1 assert_equal "Disk", exist_log["storage"]
  103. 1 assert_not_nil exist_log["file_id"]
  104. end
  105. 1 test "logs are created when deleting a file" do
  106. # Create a document with a file for testing
  107. 1 document = Document.create_with_file(
  108. filename: "delete_test.txt",
  109. content: "This is content to delete"
  110. )
  111. # Clear the log buffer before the test
  112. 1 @log_output.truncate(0)
  113. 1 @log_output.rewind
  114. # Delete the file
  115. 1 document.file.purge
  116. # Give some time for the async events to process
  117. 1 sleep(0.2)
  118. # Find delete logs in the captured output
  119. 1 delete_logs = find_log_entries("delete")
  120. 1 assert_not_empty delete_logs, "Expected delete logs to be generated"
  121. 1 delete_log = delete_logs.first
  122. 1 assert_equal "storage", delete_log["src"]
  123. 1 assert_equal "delete", delete_log["evt"]
  124. 1 assert_equal "Disk", delete_log["storage"]
  125. 1 assert_not_nil delete_log["file_id"]
  126. end
  127. 1 test "logs contain expected metadata fields" do
  128. # Clear the log buffer before the test
  129. 1 @log_output.truncate(0)
  130. 1 @log_output.rewind
  131. # Create a document with specific metadata
  132. 1 document = Document.create!
  133. # Clear the buffer again to make sure we only capture the attach operation
  134. 1 @log_output.truncate(0)
  135. 1 @log_output.rewind
  136. # Now attach the file with our known metadata
  137. 1 document.file.attach(
  138. io: StringIO.new("Test content with specific metadata"),
  139. filename: "metadata_test.txt",
  140. content_type: "text/plain"
  141. )
  142. # Give some time for the async events to process
  143. 1 sleep(0.2)
  144. # Find upload logs in the captured output
  145. 1 upload_logs = find_log_entries("upload")
  146. 1 assert_not_empty upload_logs, "Expected upload logs to be generated"
  147. 1 upload_log = upload_logs.first
  148. # Verify upload log contains the expected fields
  149. # The checksum should be present
  150. 1 assert_not_nil upload_log["checksum"]
  151. # Check file size if available - from the blob service
  152. 1 if upload_log["size"]
  153. assert_kind_of Integer, upload_log["size"]
  154. end
  155. # Check for duration which should always be present
  156. 1 assert_not_nil upload_log["duration_ms"]
  157. end
  158. end

rails_test_app/logstruct_test_app/test/integration/boot_logs_integration_test.rb

97.62% lines covered

42 relevant lines. 41 lines covered and 1 lines missed.
    
  1. # typed: true
  2. 1 require "test_helper"
  3. 1 require "open3"
  4. 1 class BootLogsIntegrationTest < ActiveSupport::TestCase
  5. 1 def test_rails_runner_emits_dotenv_structured_logs_and_ends_with_true
  6. env = {
  7. 1 "LOGSTRUCT_ENABLED" => "true",
  8. "RAILS_ENV" => "test",
  9. "RAILS_LOG_TO_STDOUT" => "1"
  10. }
  11. 1 cmd = ["bundle", "exec", "rails", "runner", "puts LogStruct.enabled?"]
  12. 1 stdout_str, stderr_str, status = Open3.capture3(env, *cmd)
  13. 1 assert_predicate status, :success?, "rails runner failed: #{stderr_str}"
  14. 1 output = stdout_str.to_s
  15. 1 refute_empty output, "Expected some output from rails runner"
  16. 1 lines = output.split("\n").map(&:strip).reject(&:empty?)
  17. 1 lines.reject! do |line|
  18. 3 line.start_with?("Coverage report generated", "Line Coverage:", "Branch Coverage:")
  19. end
  20. # Ensure the last non-empty line is 'true'
  21. 1 last_line = lines.last
  22. 1 assert_equal "true", last_line, "Expected final line to be 'true'"
  23. 1 before = lines[0...-1] || []
  24. 1 refute_empty before, "Expected logs before the final result"
  25. 1 json_logs = before.filter_map { |l|
  26. begin
  27. 2 JSON.parse(l)
  28. rescue
  29. nil
  30. end
  31. }
  32. 3 dotenv_logs = json_logs.select { |h| h["src"] == "dotenv" }
  33. 1 assert_equal 2, dotenv_logs.size, "Expected two dotenv logs"
  34. 3 assert dotenv_logs.any? { |h| h["evt"] == "load" }, "Expected a load event"
  35. 2 assert dotenv_logs.any? { |h| h["evt"] == "update" }, "Expected an update event"
  36. end
  37. 1 def test_rails_runner_emits_original_dotenv_logs_when_disabled
  38. env = {
  39. 1 "LOGSTRUCT_ENABLED" => "false",
  40. "RAILS_ENV" => "development",
  41. "RAILS_LOG_TO_STDOUT" => "1"
  42. }
  43. 1 cmd = ["bundle", "exec", "rails", "runner", "puts LogStruct.enabled?"]
  44. 1 stdout_str, stderr_str, status = Open3.capture3(env, *cmd)
  45. 1 assert_predicate status, :success?, "rails runner failed: #{stderr_str}"
  46. 1 output = stdout_str.to_s
  47. 1 refute_empty output, "Expected some output from rails runner"
  48. 1 lines = output.split("\n").map(&:strip).reject(&:empty?)
  49. 1 lines.reject! do |line|
  50. 3 line.start_with?("Coverage report generated", "Line Coverage:", "Branch Coverage:")
  51. end
  52. 1 last_line = lines.last
  53. 1 assert_equal "false", last_line, "Expected final line to be 'false'"
  54. 1 before = lines[0...-1] || []
  55. 1 refute_empty before, "Expected logs before the final result"
  56. # Expect original dotenv log lines (not JSON)
  57. 3 dotenv_lines = before.select { |l| l.start_with?("[dotenv]") }
  58. 1 assert_equal 2, dotenv_lines.size, "Expected two original dotenv lines"
  59. 2 assert dotenv_lines.any? { |l| l.include?("Set ") }, "Expected a 'Set ...' line"
  60. 3 assert dotenv_lines.any? { |l| l.include?("Loaded ") }, "Expected a 'Loaded ...' line"
  61. end
  62. end

rails_test_app/logstruct_test_app/test/integration/dotenv_integration_test.rb

95.24% lines covered

21 relevant lines. 20 lines covered and 1 lines missed.
    
  1. # typed: true
  2. 1 require "test_helper"
  3. 1 class DotenvIntegrationTest < ActiveSupport::TestCase
  4. 1 def setup
  5. 1 @io = StringIO.new
  6. 1 ::SemanticLogger.clear_appenders!
  7. 1 ::SemanticLogger.add_appender(io: @io, formatter: LogStruct::SemanticLogger::Formatter.new, async: false)
  8. end
  9. 1 def test_emits_structured_dotenv_logs_and_suppresses_unstructured_messages
  10. # Simulate a dotenv update event after boot
  11. 1 diff = Struct.new(:env).new({"BOOT_FLAG" => "1", "REGION" => "ap-southeast-2"})
  12. 1 ActiveSupport::Notifications.instrument("update.dotenv", diff: diff) {}
  13. 1 ::SemanticLogger.flush
  14. 1 @io.rewind
  15. 1 lines = @io.read.to_s.split("\n").map(&:strip).reject(&:empty?)
  16. 1 refute_empty lines, "Expected logs to be captured during test"
  17. 1 json_logs = lines.filter_map { |l|
  18. begin
  19. 1 JSON.parse(l)
  20. rescue
  21. nil
  22. end
  23. }
  24. 2 dotenv_updates = json_logs.select { |h| h["src"] == "dotenv" && h["evt"] == "update" }
  25. 1 refute_empty dotenv_updates, "Expected a structured dotenv update log"
  26. # Vars should include at least BOOT_FLAG
  27. 2 assert dotenv_updates.any? { |h| Array(h["vars"]).include?("BOOT_FLAG") }, "Expected BOOT_FLAG in vars"
  28. # Ensure no plain unstructured "Set ..." messages slipped through
  29. 2 no_unstructured = json_logs.none? { |h| h["msg"].is_a?(String) && h["msg"].start_with?("Set ") }
  30. 1 assert no_unstructured, "Found unstructured 'Set ...' message in logs"
  31. end
  32. end

rails_test_app/logstruct_test_app/test/integration/host_authorization_test.rb

92.73% lines covered

55 relevant lines. 51 lines covered and 4 lines missed.
    
  1. # typed: true
  2. # frozen_string_literal: true
  3. 1 require "test_helper"
  4. 1 class HostAuthorizationTest < ActionDispatch::IntegrationTest
  5. 1 def setup
  6. # Capture JSON output via a dedicated SemanticLogger appender
  7. 3 @io = StringIO.new
  8. 3 ::SemanticLogger.clear_appenders!
  9. # Use synchronous appender to avoid timing issues in tests
  10. 3 ::SemanticLogger.add_appender(io: @io, formatter: LogStruct::SemanticLogger::Formatter.new, async: false)
  11. end
  12. 1 def test_blocked_host_is_logged_with_logstruct
  13. # Make a request with a blocked host
  14. 1 host! "blocked-host.example.com"
  15. 1 get "/health"
  16. # Should return 403 Forbidden
  17. 1 assert_response :forbidden
  18. # Ensure all logs are flushed from buffers
  19. 1 ::SemanticLogger.flush
  20. # Read all logged lines
  21. 1 @io.rewind
  22. 1 lines = @io.read.to_s.split("\n").map(&:strip).reject(&:empty?)
  23. # Parse JSON logs
  24. 1 parsed_logs = lines.filter_map { |l|
  25. begin
  26. 1 JSON.parse(l)
  27. rescue
  28. nil
  29. end
  30. }
  31. # Find blocked host logs
  32. 2 blocked_host_logs = parsed_logs.select { |log| log["evt"] == "blocked_host" }
  33. 1 assert_equal 1, blocked_host_logs.size, "Expected exactly one blocked host log entry"
  34. 1 log_entry = blocked_host_logs.first
  35. # Verify the log entry has the correct structure
  36. 1 assert_equal "security", log_entry["src"]
  37. 1 assert_equal "blocked_host", log_entry["evt"]
  38. 1 assert_equal "blocked-host.example.com", log_entry["blocked_host"]
  39. 1 assert_equal "/health", log_entry["path"]
  40. 1 assert_equal "GET", log_entry["method"]
  41. end
  42. 1 def test_allowed_host_is_not_blocked
  43. # Make a request with an allowed host (.localhost is allowed by default)
  44. 1 host! "www.localhost"
  45. 1 get "/health"
  46. # Should return 200 OK
  47. 1 assert_response :success
  48. # Ensure all logs are flushed from buffers
  49. 1 ::SemanticLogger.flush
  50. # Read all logged lines
  51. 1 @io.rewind
  52. 1 lines = @io.read.to_s.split("\n").map(&:strip).reject(&:empty?)
  53. # Parse JSON logs
  54. 1 parsed_logs = lines.filter_map { |l|
  55. begin
  56. JSON.parse(l)
  57. rescue
  58. nil
  59. end
  60. }
  61. # Find blocked host logs
  62. 1 blocked_host_logs = parsed_logs.select { |log| log["evt"] == "blocked_host" }
  63. 1 assert_equal 0, blocked_host_logs.size, "Should not log blocked host for allowed hosts"
  64. end
  65. 1 def test_blocked_host_log_can_be_serialized
  66. 1 host! "malicious.example.com"
  67. 1 get "/health"
  68. 1 assert_response :forbidden
  69. # Ensure all logs are flushed from buffers
  70. 1 ::SemanticLogger.flush
  71. # Read all logged lines
  72. 1 @io.rewind
  73. 1 lines = @io.read.to_s.split("\n").map(&:strip).reject(&:empty?)
  74. # Parse JSON logs
  75. 1 parsed_logs = lines.filter_map { |l|
  76. begin
  77. 1 JSON.parse(l)
  78. rescue
  79. nil
  80. end
  81. }
  82. # Find blocked host logs
  83. 2 blocked_host_logs = parsed_logs.select { |log| log["evt"] == "blocked_host" }
  84. 1 assert_equal 1, blocked_host_logs.size
  85. 1 log_entry = blocked_host_logs.first
  86. # Verify it's a properly serialized hash
  87. 1 assert_kind_of Hash, log_entry
  88. # Verify key fields are in serialized output
  89. 1 assert_equal "security", log_entry["src"]
  90. 1 assert_equal "blocked_host", log_entry["evt"]
  91. 1 assert_equal "malicious.example.com", log_entry["blocked_host"]
  92. 1 assert_equal "/health", log_entry["path"]
  93. 1 assert_equal "GET", log_entry["method"]
  94. end
  95. end

rails_test_app/logstruct_test_app/test/integration/logging_integration_test.rb

100.0% lines covered

36 relevant lines. 36 lines covered and 0 lines missed.
    
  1. # typed: true
  2. # frozen_string_literal: true
  3. 1 require "test_helper"
  4. 1 class LoggingIntegrationTest < ActionDispatch::IntegrationTest
  5. # Basic test to ensure the Rails app is working
  6. 1 def test_healthcheck_works
  7. 1 get "/health"
  8. 1 assert_response :success
  9. 1 assert_equal "OK", response.body
  10. end
  11. # More detailed test to verify basic logging
  12. 1 def test_basic_logging_endpoint_works
  13. 1 get "/logging/basic"
  14. 1 assert_response :success
  15. 1 response_json = JSON.parse(response.body)
  16. 1 assert_equal "ok", response_json["status"]
  17. 1 assert_equal "Basic logging completed", response_json["message"]
  18. end
  19. # Test error logging
  20. 1 def test_error_logging_endpoint_works
  21. # The error will be raised and we should see it
  22. 1 error_raised = false
  23. begin
  24. 1 get "/logging/error"
  25. rescue RuntimeError => e
  26. 1 error_raised = true
  27. 1 assert_equal "Test error for integration testing", e.message
  28. end
  29. 1 assert error_raised, "Expected an error to be raised"
  30. end
  31. # Test custom log structures
  32. 1 def test_custom_log_class_work
  33. 1 get "/logging/custom"
  34. 1 assert_response :success
  35. 1 response_json = JSON.parse(response.body)
  36. 1 assert_equal "ok", response_json["status"]
  37. 1 assert_equal "Custom logging completed", response_json["message"]
  38. end
  39. # Test request logging
  40. 1 def test_request_logging_works
  41. 1 get "/logging/request"
  42. 1 assert_response :success
  43. 1 response_json = JSON.parse(response.body)
  44. 1 assert_equal "ok", response_json["status"]
  45. 1 assert_equal "Request logging completed", response_json["message"]
  46. end
  47. # Test that error handling is stack-safe
  48. 1 def test_error_logging
  49. # This test intentionally creates a situation that would cause
  50. # an infinite loop if error handling is not implemented correctly
  51. 1 get "/logging/error_logging"
  52. 1 assert_response :success
  53. 1 response_json = JSON.parse(response.body)
  54. 1 assert_equal "ok", response_json["status"]
  55. 1 assert_equal "Stack-safe error handling test completed", response_json["message"]
  56. end
  57. end

rails_test_app/logstruct_test_app/test/integration/lograge_formatter_integration_test.rb

95.24% lines covered

21 relevant lines. 20 lines covered and 1 lines missed.
    
  1. # typed: true
  2. 1 require "test_helper"
  3. 1 class LogrageFormatterIntegrationTest < ActionDispatch::IntegrationTest
  4. 1 def setup
  5. # Capture JSON output via a dedicated SemanticLogger appender
  6. 1 @io = StringIO.new
  7. 1 ::SemanticLogger.clear_appenders!
  8. # Use synchronous appender to avoid timing issues in tests
  9. 1 ::SemanticLogger.add_appender(io: @io, formatter: LogStruct::SemanticLogger::Formatter.new, async: false)
  10. end
  11. 1 def test_request_through_stack_emits_json_request_log
  12. 1 get "/logging/basic", params: {format: :json}
  13. 1 assert_response :success
  14. # Ensure all logs are flushed from buffers
  15. 1 ::SemanticLogger.flush
  16. # Read all logged lines
  17. 1 @io.rewind
  18. 1 lines = @io.read.to_s.split("\n").map(&:strip).reject(&:empty?)
  19. 1 refute_empty lines, "Expected some JSON logs to be emitted"
  20. # Find the request log entry
  21. 1 request_log = lines.filter_map { |l|
  22. begin
  23. 6 JSON.parse(l)
  24. rescue
  25. nil
  26. end
  27. 6 }.find { |h| h["evt"] == "request" }
  28. 1 refute_nil request_log, "Expected a request log entry"
  29. # Validate normalized types
  30. 1 assert_equal "GET", request_log["method"]
  31. 1 assert_equal "json", request_log["format"]
  32. 1 assert_kind_of Hash, request_log["params"]
  33. end
  34. end

rails_test_app/logstruct_test_app/test/integration/puma_integration_test.rb

97.92% lines covered

48 relevant lines. 47 lines covered and 1 lines missed.
    
  1. # typed: true
  2. 1 require "test_helper"
  3. 1 require "open3"
  4. 1 require "timeout"
  5. 1 class PumaIntegrationTest < ActiveSupport::TestCase
  6. 1 def test_rails_server_emits_structured_puma_logs_and_on_exit
  7. 1 port = 32123
  8. env = {
  9. 1 "LOGSTRUCT_ENABLED" => "true",
  10. "RAILS_ENV" => "test",
  11. "RAILS_LOG_TO_STDOUT" => "1"
  12. }
  13. 1 cmd = ["bundle", "exec", "rails", "server", "-p", port.to_s]
  14. 1 Open3.popen3(env, *cmd) do |_stdin, stdout, stderr, wait_thr| # cspell:disable-line
  15. begin
  16. 1 lines = []
  17. 1 Timeout.timeout(10) do
  18. 8 while (line = stdout.gets)
  19. 7 lines << line.strip
  20. 7 break if line.include?("Use Ctrl-C to stop")
  21. end
  22. end
  23. # Send TERM to trigger graceful shutdown
  24. begin
  25. 1 Process.kill("TERM", wait_thr.pid)
  26. rescue Errno::ESRCH
  27. # Process already exited
  28. end
  29. # Collect shutdown output
  30. 1 Timeout.timeout(10) do
  31. 4 while (line = stdout.gets)
  32. 2 lines << line.strip
  33. end
  34. end
  35. rescue Timeout::Error
  36. # Fall through and ensure process is terminated
  37. ensure
  38. begin
  39. 1 Process.kill("TERM", wait_thr.pid)
  40. rescue Errno::ESRCH
  41. # already dead
  42. end
  43. end
  44. 1 output = lines.join("\n")
  45. 1 lines.filter_map { |l|
  46. begin
  47. 9 JSON.parse(l)
  48. rescue
  49. 5 nil
  50. end
  51. }
  52. # Consider only logs after the first JSON line
  53. 1 first_json_index = lines.find_index { |l|
  54. 4 l.strip.start_with?("{") && begin
  55. 1 JSON.parse(l)
  56. rescue
  57. nil
  58. end
  59. }
  60. 1 assert first_json_index, "Did not find any JSON log lines. Output: #{output}\nSTDERR: #{stderr.read}"
  61. 1 after_lines = lines[first_json_index..]
  62. 1 after_json = after_lines.filter_map do |l|
  63. 6 JSON.parse(l)
  64. rescue JSON::ParserError
  65. 2 nil
  66. end
  67. 5 puma_logs = after_json.select { |h| h["src"] == "puma" }
  68. # Expect exactly 2 structured logs: start, shutdown
  69. 1 assert_equal 2, puma_logs.length, "Expected exactly 2 Puma logs. Output: #{output}\nSTDERR: #{stderr.read}"
  70. 3 events = puma_logs.map { |h| h["evt"] }
  71. 1 assert_equal ["start", "shutdown"], events, "Expected Puma events in order: start, shutdown"
  72. 1 start = puma_logs[0]
  73. 1 assert_equal "puma", start["src"]
  74. 1 assert_equal "info", start["lvl"]
  75. 1 assert_equal "single", start["mode"]
  76. 1 assert_equal "test", start["environment"]
  77. 1 assert_kind_of Integer, start["pid"]
  78. 1 assert_kind_of Array, start["listening_addresses"]
  79. 2 assert start["listening_addresses"].any? { |a| a.include?(":#{port}") }, "Expected listening address to include :#{port}"
  80. 1 shutdown = puma_logs[1]
  81. 1 assert_equal "puma", shutdown["src"]
  82. 1 assert_equal "info", shutdown["lvl"]
  83. 1 assert_kind_of Integer, shutdown["pid"]
  84. end
  85. end
  86. end

rails_test_app/logstruct_test_app/test/integration/test_logging_integration_test.rb

82.35% lines covered

34 relevant lines. 28 lines covered and 6 lines missed.
    
  1. # typed: true
  2. 1 require "test_helper"
  3. 1 require "open3"
  4. 1 require "timeout"
  5. 1 require "fileutils"
  6. 1 class TestLoggingIntegrationTest < ActiveSupport::TestCase
  7. 1 def test_test_logs_go_to_file_not_stdout
  8. # Clean up log file before test
  9. 1 log_file = Rails.root.join("log/test.log")
  10. 1 FileUtils.rm_f(log_file)
  11. 1 FileUtils.touch(log_file)
  12. env = {
  13. 1 "LOGSTRUCT_ENABLED" => "true",
  14. "RAILS_ENV" => "test"
  15. }
  16. # Run a simple test that will generate logs
  17. 1 cmd = ["bundle", "exec", "rails", "test", "test/models/user_test.rb"]
  18. 1 Open3.popen3(env, *cmd, chdir: Rails.root.to_s) do |_stdin, stdout, stderr, wait_thr|
  19. begin
  20. 1 Timeout.timeout(30) do
  21. 1 wait_thr.value # Wait for process to complete
  22. end
  23. rescue Timeout::Error
  24. begin
  25. Process.kill("TERM", wait_thr.pid)
  26. rescue
  27. nil
  28. end
  29. flunk "Test process timed out"
  30. end
  31. 1 stdout_output = stdout.read
  32. 1 stderr.read
  33. # Check that stdout doesn't contain JSON logs
  34. 1 json_lines_in_stdout = stdout_output.lines.select { |line|
  35. 7 line.strip.start_with?("{") && begin
  36. JSON.parse(line)
  37. rescue
  38. nil
  39. end
  40. }
  41. 1 assert_equal 0,
  42. json_lines_in_stdout.length,
  43. "Expected no JSON logs in stdout, but found #{json_lines_in_stdout.length} lines. First few:\n#{json_lines_in_stdout.first(3).join}"
  44. # Check that log/test.log contains JSON logs
  45. 1 assert_path_exists log_file, "Expected log/test.log to exist"
  46. 1 log_contents = File.read(log_file)
  47. 1 json_lines_in_file = log_contents.lines.select { |line|
  48. 11 line.strip.start_with?("{") && begin
  49. 11 JSON.parse(line)
  50. rescue
  51. nil
  52. end
  53. }
  54. 1 assert_operator json_lines_in_file.length, :>, 0, "Expected JSON logs in log/test.log, but found none. File size: #{log_contents.bytesize} bytes"
  55. # Verify at least one structured log exists
  56. 12 parsed_logs = json_lines_in_file.map { |line| JSON.parse(line) }
  57. 2 assert parsed_logs.any? { |log| log["src"] && log["evt"] && log["lvl"] },
  58. "Expected at least one properly structured log in log/test.log"
  59. end
  60. ensure
  61. # Clean up
  62. 1 FileUtils.rm_f(log_file) if log_file
  63. end
  64. end

rails_test_app/logstruct_test_app/test/models/user_test.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. # typed: true
  2. 1 require "test_helper"
  3. 1 class UserTest < ActiveSupport::TestCase
  4. 1 test "simple test that generates logs" do # rubocop:disable Minitest/NoAssertions
  5. # This test just needs to run and generate some logs
  6. 1 Rails.logger.info("Test log message")
  7. end
  8. end

rails_test_app/logstruct_test_app/test/test_helper.rb

53.33% lines covered

30 relevant lines. 16 lines covered and 14 lines missed.
    
  1. # typed: true
  2. 1 require "simplecov" unless defined?(SimpleCov)
  3. 1 require "simplecov-json"
  4. 1 require "sorbet-runtime"
  5. 1 require "debug"
  6. 1 unless SimpleCov.running
  7. SimpleCov.formatters = [
  8. SimpleCov::Formatter::HTMLFormatter,
  9. SimpleCov::Formatter::JSONFormatter
  10. ]
  11. SimpleCov.start do
  12. T.bind(self, T.all(SimpleCov::Configuration, Kernel))
  13. gem_path = File.expand_path("../../../../", __FILE__)
  14. SimpleCov.root(gem_path)
  15. add_filter "rails_test_app"
  16. coverage_dir "coverage_rails"
  17. enable_coverage :branch
  18. primary_coverage :branch
  19. end
  20. SimpleCov.at_exit do
  21. SimpleCov.result
  22. end
  23. end
  24. # Require logstruct after starting SimpleCov
  25. 1 require "logstruct"
  26. 1 ENV["RAILS_ENV"] ||= "test"
  27. 1 require_relative "../config/environment"
  28. 1 require "rails/test_help"
  29. 1 require "minitest/reporters"
  30. # Configure colorful test output
  31. 1 Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
  32. # Configure the test database
  33. 1 class ActiveSupport::TestCase
  34. # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
  35. # fixtures :all
  36. # Add more helper methods to be used by all tests here...
  37. # Helper method to run jobs synchronously
  38. 1 def perform_enqueued_jobs
  39. jobs = ActiveJob::Base.queue_adapter.enqueued_jobs
  40. jobs.each do |job|
  41. ActiveJob::Base.execute job
  42. end
  43. end
  44. end
  45. # Ensure LogStruct is enabled and emits JSON in tests across Rails versions
  46. begin
  47. 1 LogStruct.configure do |config|
  48. 1 config.enabled = true
  49. # Prefer production-style JSON in development/test
  50. 1 config.prefer_json_in_development = true
  51. end
  52. rescue NameError
  53. # LogStruct not loaded; ignore
  54. end