logstruct 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -1
  3. data/README.md +15 -2
  4. data/lib/log_struct/boot_buffer.rb +28 -0
  5. data/lib/log_struct/builders/active_job.rb +84 -0
  6. data/lib/log_struct/concerns/configuration.rb +126 -13
  7. data/lib/log_struct/concerns/error_handling.rb +3 -7
  8. data/lib/log_struct/config_struct/filters.rb +18 -0
  9. data/lib/log_struct/config_struct/integrations.rb +8 -12
  10. data/lib/log_struct/configuration.rb +13 -0
  11. data/lib/log_struct/enums/event.rb +13 -0
  12. data/lib/log_struct/enums/log_field.rb +154 -0
  13. data/lib/log_struct/enums/source.rb +4 -1
  14. data/lib/log_struct/formatter.rb +29 -17
  15. data/lib/log_struct/integrations/action_mailer/error_handling.rb +3 -11
  16. data/lib/log_struct/integrations/action_mailer/event_logging.rb +22 -12
  17. data/lib/log_struct/integrations/active_job/log_subscriber.rb +52 -48
  18. data/lib/log_struct/integrations/active_model_serializers.rb +8 -14
  19. data/lib/log_struct/integrations/active_record.rb +35 -5
  20. data/lib/log_struct/integrations/active_storage.rb +59 -20
  21. data/lib/log_struct/integrations/ahoy.rb +2 -1
  22. data/lib/log_struct/integrations/carrierwave.rb +13 -16
  23. data/lib/log_struct/integrations/dotenv.rb +278 -0
  24. data/lib/log_struct/integrations/good_job/log_subscriber.rb +86 -136
  25. data/lib/log_struct/integrations/good_job/logger.rb +8 -10
  26. data/lib/log_struct/integrations/good_job.rb +5 -7
  27. data/lib/log_struct/integrations/host_authorization.rb +25 -4
  28. data/lib/log_struct/integrations/lograge.rb +20 -14
  29. data/lib/log_struct/integrations/puma.rb +482 -0
  30. data/lib/log_struct/integrations/rack_error_handler/middleware.rb +11 -18
  31. data/lib/log_struct/integrations/shrine.rb +44 -19
  32. data/lib/log_struct/integrations/sorbet.rb +48 -0
  33. data/lib/log_struct/integrations.rb +21 -0
  34. data/lib/log_struct/log/action_mailer/delivered.rb +99 -0
  35. data/lib/log_struct/log/action_mailer/delivery.rb +99 -0
  36. data/lib/log_struct/log/action_mailer.rb +30 -45
  37. data/lib/log_struct/log/active_job/enqueue.rb +125 -0
  38. data/lib/log_struct/log/active_job/finish.rb +130 -0
  39. data/lib/log_struct/log/active_job/schedule.rb +125 -0
  40. data/lib/log_struct/log/active_job/start.rb +130 -0
  41. data/lib/log_struct/log/active_job.rb +41 -54
  42. data/lib/log_struct/log/active_model_serializers.rb +72 -33
  43. data/lib/log_struct/log/active_storage/delete.rb +87 -0
  44. data/lib/log_struct/log/active_storage/download.rb +103 -0
  45. data/lib/log_struct/log/active_storage/exist.rb +93 -0
  46. data/lib/log_struct/log/active_storage/metadata.rb +93 -0
  47. data/lib/log_struct/log/active_storage/stream.rb +93 -0
  48. data/lib/log_struct/log/active_storage/upload.rb +118 -0
  49. data/lib/log_struct/log/active_storage/url.rb +93 -0
  50. data/lib/log_struct/log/active_storage.rb +32 -68
  51. data/lib/log_struct/log/ahoy.rb +67 -33
  52. data/lib/log_struct/log/carrierwave/delete.rb +115 -0
  53. data/lib/log_struct/log/carrierwave/download.rb +131 -0
  54. data/lib/log_struct/log/carrierwave/upload.rb +141 -0
  55. data/lib/log_struct/log/carrierwave.rb +37 -72
  56. data/lib/log_struct/log/dotenv/load.rb +76 -0
  57. data/lib/log_struct/log/dotenv/restore.rb +76 -0
  58. data/lib/log_struct/log/dotenv/save.rb +76 -0
  59. data/lib/log_struct/log/dotenv/update.rb +76 -0
  60. data/lib/log_struct/log/dotenv.rb +12 -0
  61. data/lib/log_struct/log/error.rb +58 -47
  62. data/lib/log_struct/log/good_job/enqueue.rb +126 -0
  63. data/lib/log_struct/log/good_job/error.rb +151 -0
  64. data/lib/log_struct/log/good_job/finish.rb +136 -0
  65. data/lib/log_struct/log/good_job/log.rb +131 -0
  66. data/lib/log_struct/log/good_job/schedule.rb +136 -0
  67. data/lib/log_struct/log/good_job/start.rb +136 -0
  68. data/lib/log_struct/log/good_job.rb +40 -141
  69. data/lib/log_struct/log/interfaces/additional_data_field.rb +1 -17
  70. data/lib/log_struct/log/interfaces/common_fields.rb +1 -39
  71. data/lib/log_struct/log/interfaces/public_common_fields.rb +1 -28
  72. data/lib/log_struct/log/interfaces/request_fields.rb +1 -33
  73. data/lib/log_struct/log/plain.rb +59 -34
  74. data/lib/log_struct/log/puma/shutdown.rb +80 -0
  75. data/lib/log_struct/log/puma/start.rb +120 -0
  76. data/lib/log_struct/log/puma.rb +10 -0
  77. data/lib/log_struct/log/request.rb +132 -48
  78. data/lib/log_struct/log/security/blocked_host.rb +141 -0
  79. data/lib/log_struct/log/security/csrf_violation.rb +131 -0
  80. data/lib/log_struct/log/security/ip_spoof.rb +141 -0
  81. data/lib/log_struct/log/security.rb +40 -70
  82. data/lib/log_struct/log/shared/add_request_fields.rb +1 -26
  83. data/lib/log_struct/log/shared/merge_additional_data_fields.rb +1 -22
  84. data/lib/log_struct/log/shared/serialize_common.rb +1 -33
  85. data/lib/log_struct/log/shared/serialize_common_public.rb +9 -9
  86. data/lib/log_struct/log/shrine/delete.rb +85 -0
  87. data/lib/log_struct/log/shrine/download.rb +90 -0
  88. data/lib/log_struct/log/shrine/exist.rb +90 -0
  89. data/lib/log_struct/log/shrine/metadata.rb +90 -0
  90. data/lib/log_struct/log/shrine/upload.rb +105 -0
  91. data/lib/log_struct/log/shrine.rb +10 -67
  92. data/lib/log_struct/log/sidekiq.rb +65 -26
  93. data/lib/log_struct/log/sql.rb +113 -106
  94. data/lib/log_struct/log.rb +29 -36
  95. data/lib/log_struct/multi_error_reporter.rb +80 -22
  96. data/lib/log_struct/param_filters.rb +50 -7
  97. data/lib/log_struct/rails_boot_banner_silencer.rb +123 -0
  98. data/lib/log_struct/railtie.rb +71 -0
  99. data/lib/log_struct/semantic_logger/formatter.rb +4 -2
  100. data/lib/log_struct/semantic_logger/setup.rb +34 -18
  101. data/lib/log_struct/shared/interfaces/additional_data_field.rb +22 -0
  102. data/lib/log_struct/shared/interfaces/common_fields.rb +39 -0
  103. data/lib/log_struct/shared/interfaces/public_common_fields.rb +29 -0
  104. data/lib/log_struct/shared/interfaces/request_fields.rb +39 -0
  105. data/lib/log_struct/shared/shared/add_request_fields.rb +28 -0
  106. data/lib/log_struct/shared/shared/merge_additional_data_fields.rb +27 -0
  107. data/lib/log_struct/shared/shared/serialize_common.rb +58 -0
  108. data/lib/log_struct/version.rb +1 -1
  109. data/lib/log_struct.rb +22 -4
  110. data/logstruct.gemspec +2 -1
  111. metadata +78 -9
  112. data/lib/log_struct/log/interfaces/message_field.rb +0 -20
  113. data/lib/log_struct/log_keys.rb +0 -102
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative "enums/error_reporter"
5
+ require_relative "handlers"
5
6
 
6
7
  # Try to require all supported error reporting libraries
7
8
  # Users may have multiple installed, so we should load all of them
@@ -19,33 +20,62 @@ module LogStruct
19
20
  # but the operation should be allowed to continue (e.g. scrubbing log data.)
20
21
  class MultiErrorReporter
21
22
  # Class variable to store the selected reporter
22
- @reporter = T.let(nil, T.nilable(ErrorReporter))
23
+ class CallableReporterWrapper
24
+ extend T::Sig
25
+
26
+ sig { params(callable: T.untyped).void }
27
+ def initialize(callable)
28
+ @callable = callable
29
+ end
30
+
31
+ sig { returns(T.untyped) }
32
+ attr_reader :callable
33
+ alias_method :original, :callable
34
+
35
+ sig { params(error: StandardError, context: T.nilable(T::Hash[Symbol, T.untyped]), source: Source).void }
36
+ def call(error, context, source)
37
+ case callable_arity
38
+ when 3
39
+ callable.call(error, context, source)
40
+ when 2
41
+ callable.call(error, context)
42
+ when 1
43
+ callable.call(error)
44
+ else
45
+ callable.call(error, context, source)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ sig { returns(Integer) }
52
+ def callable_arity
53
+ callable.respond_to?(:arity) ? callable.arity : -1
54
+ end
55
+ end
56
+
57
+ ReporterImpl = T.type_alias { T.any(ErrorReporter, CallableReporterWrapper) }
58
+
59
+ @reporter_impl = T.let(nil, T.nilable(ReporterImpl))
23
60
 
24
61
  class << self
25
62
  extend T::Sig
26
63
 
27
- sig { returns(ErrorReporter) }
64
+ sig { returns(ReporterImpl) }
28
65
  def reporter
29
- @reporter ||= detect_reporter
66
+ reporter_impl
30
67
  end
31
68
 
32
69
  # Set the reporter to use (user-friendly API that accepts symbols)
33
- sig { params(reporter_type: T.any(ErrorReporter, Symbol)).returns(ErrorReporter) }
70
+ sig { params(reporter_type: T.any(ErrorReporter, Symbol, Handlers::ErrorReporter)).returns(ReporterImpl) }
34
71
  def reporter=(reporter_type)
35
- @reporter = case reporter_type
72
+ @reporter_impl = case reporter_type
36
73
  when ErrorReporter
37
74
  reporter_type
38
75
  when Symbol
39
- case reporter_type
40
- when :sentry then ErrorReporter::Sentry
41
- when :bugsnag then ErrorReporter::Bugsnag
42
- when :rollbar then ErrorReporter::Rollbar
43
- when :honeybadger then ErrorReporter::Honeybadger
44
- when :rails_logger then ErrorReporter::RailsLogger
45
- else
46
- valid_types = ErrorReporter.values.map { |v| ":#{v.serialize}" }.join(", ")
47
- raise ArgumentError, "Unknown reporter type: #{reporter_type}. Valid types are: #{valid_types}"
48
- end
76
+ resolve_symbol_reporter(reporter_type)
77
+ else
78
+ wrap_callable_reporter(reporter_type)
49
79
  end
50
80
  end
51
81
 
@@ -69,7 +99,9 @@ module LogStruct
69
99
  sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
70
100
  def report_error(error, context = {})
71
101
  # Call the appropriate reporter method based on what's available
72
- case reporter
102
+ impl = reporter_impl
103
+
104
+ case impl
73
105
  when ErrorReporter::Sentry
74
106
  report_to_sentry(error, context)
75
107
  when ErrorReporter::Bugsnag
@@ -78,13 +110,43 @@ module LogStruct
78
110
  report_to_rollbar(error, context)
79
111
  when ErrorReporter::Honeybadger
80
112
  report_to_honeybadger(error, context)
81
- else
113
+ when ErrorReporter::RailsLogger
82
114
  fallback_logging(error, context)
115
+ when CallableReporterWrapper
116
+ impl.call(error, context, Source::Internal)
83
117
  end
84
118
  end
85
119
 
86
120
  private
87
121
 
122
+ sig { returns(ReporterImpl) }
123
+ def reporter_impl
124
+ @reporter_impl ||= detect_reporter
125
+ end
126
+
127
+ sig { params(symbol: Symbol).returns(ErrorReporter) }
128
+ def resolve_symbol_reporter(symbol)
129
+ case symbol
130
+ when :sentry then ErrorReporter::Sentry
131
+ when :bugsnag then ErrorReporter::Bugsnag
132
+ when :rollbar then ErrorReporter::Rollbar
133
+ when :honeybadger then ErrorReporter::Honeybadger
134
+ when :rails_logger then ErrorReporter::RailsLogger
135
+ else
136
+ valid_types = ErrorReporter.values.map { |v| ":#{v.serialize}" }.join(", ")
137
+ raise ArgumentError, "Unknown reporter type: #{symbol}. Valid types are: #{valid_types}"
138
+ end
139
+ end
140
+
141
+ sig { params(callable: T.untyped).returns(CallableReporterWrapper) }
142
+ def wrap_callable_reporter(callable)
143
+ unless callable.respond_to?(:call)
144
+ raise ArgumentError, "Reporter must respond to #call"
145
+ end
146
+
147
+ CallableReporterWrapper.new(callable)
148
+ end
149
+
88
150
  # Report to Sentry
89
151
  sig { params(error: StandardError, context: T::Hash[T.untyped, T.untyped]).void }
90
152
  def report_to_sentry(error, context = {})
@@ -135,11 +197,7 @@ module LogStruct
135
197
  return if error.nil?
136
198
 
137
199
  # Create a proper error log entry
138
- error_log = Log::Error.from_exception(
139
- Source::LogStruct,
140
- error,
141
- context
142
- )
200
+ error_log = Log.from_exception(Source::Internal, error, context)
143
201
 
144
202
  # Use LogStruct.error to properly log the error
145
203
  LogStruct.error(error_log)
@@ -3,6 +3,8 @@
3
3
 
4
4
  require "digest"
5
5
  require_relative "hash_utils"
6
+ require_relative "config_struct/filters"
7
+ require_relative "enums/source"
6
8
 
7
9
  module LogStruct
8
10
  # This class contains methods for filtering sensitive data in logs
@@ -12,19 +14,30 @@ module LogStruct
12
14
  extend T::Sig
13
15
 
14
16
  # Check if a key should be filtered based on our defined sensitive keys
15
- sig { params(key: T.any(String, Symbol)).returns(T::Boolean) }
16
- def should_filter_key?(key)
17
- LogStruct.config.filters.filter_keys.include?(key.to_s.downcase.to_sym)
17
+ sig { params(key: T.untyped, value: T.untyped).returns(T::Boolean) }
18
+ def should_filter_key?(key, value = nil)
19
+ filters = LogStruct.config.filters
20
+ normalized_key = key.to_s
21
+ normalized_symbol = normalized_key.downcase.to_sym
22
+
23
+ return true if filters.filter_keys.include?(normalized_symbol)
24
+
25
+ filters.filter_matchers.any? do |matcher|
26
+ matcher.matches?(normalized_key, value)
27
+ rescue => e
28
+ handle_filter_matcher_error(e, matcher, normalized_key)
29
+ false
30
+ end
18
31
  end
19
32
 
20
33
  # Check if a key should be hashed rather than completely filtered
21
- sig { params(key: T.any(String, Symbol)).returns(T::Boolean) }
34
+ sig { params(key: T.untyped).returns(T::Boolean) }
22
35
  def should_include_string_hash?(key)
23
36
  LogStruct.config.filters.filter_keys_with_hashes.include?(key.to_s.downcase.to_sym)
24
37
  end
25
38
 
26
39
  # Convert a value to a filtered summary hash (e.g. { _filtered: { class: "String", ... }})
27
- sig { params(key: T.any(String, Symbol), data: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
40
+ sig { params(key: T.untyped, data: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
28
41
  def summarize_json_attribute(key, data)
29
42
  case data
30
43
  when Hash
@@ -59,12 +72,18 @@ module LogStruct
59
72
  return {_class: "Hash", _empty: true} if hash.empty?
60
73
 
61
74
  # Don't include byte size if hash contains any filtered keys
62
- has_sensitive_keys = hash.keys.any? { |key| should_filter_key?(key) }
75
+ has_sensitive_keys = T.let(false, T::Boolean)
76
+ normalized_keys = []
77
+
78
+ hash.each do |key, value|
79
+ has_sensitive_keys ||= should_filter_key?(key, value)
80
+ normalized_keys << normalize_summary_key(key)
81
+ end
63
82
 
64
83
  summary = {
65
84
  _class: Hash,
66
85
  _keys_count: hash.keys.size,
67
- _keys: hash.keys.map(&:to_sym).take(10)
86
+ _keys: normalized_keys.take(10)
68
87
  }
69
88
 
70
89
  # Only add byte size if no sensitive keys are present
@@ -84,6 +103,30 @@ module LogStruct
84
103
  _bytes: array.to_json.bytesize
85
104
  }
86
105
  end
106
+
107
+ private
108
+
109
+ sig { params(key: T.any(String, Symbol, Integer, T.untyped)).returns(T.any(Symbol, String)) }
110
+ def normalize_summary_key(key)
111
+ if key.is_a?(Symbol)
112
+ key
113
+ elsif key.respond_to?(:to_sym)
114
+ key.to_sym
115
+ else
116
+ key.to_s
117
+ end
118
+ rescue
119
+ key.to_s
120
+ end
121
+
122
+ sig { params(error: StandardError, matcher: ConfigStruct::FilterMatcher, key: String).void }
123
+ def handle_filter_matcher_error(error, matcher, key)
124
+ context = {
125
+ matcher: matcher.label,
126
+ key: key
127
+ }
128
+ LogStruct.handle_exception(error, source: Source::Internal, context: context)
129
+ end
87
130
  end
88
131
  end
89
132
  end
@@ -0,0 +1,123 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module LogStruct
7
+ module RailsBootBannerSilencer
8
+ extend T::Sig
9
+
10
+ @installed = T.let(false, T::Boolean)
11
+
12
+ sig { void }
13
+ def self.install!
14
+ return if @installed
15
+ @installed = true
16
+
17
+ return unless ARGV.include?("server")
18
+ patch!
19
+ end
20
+
21
+ sig { returns(T::Boolean) }
22
+ def self.patch!
23
+ begin
24
+ require "rails/command"
25
+ require "rails/commands/server/server_command"
26
+ rescue LoadError
27
+ # Best-effort – if Rails isn't available yet we'll try again later
28
+ return false
29
+ end
30
+
31
+ server_command = T.let(nil, T.untyped)
32
+ # rubocop:disable Sorbet/ConstantsFromStrings
33
+ begin
34
+ server_command = ::Object.const_get("Rails::Command::ServerCommand")
35
+ rescue NameError
36
+ server_command = nil
37
+ end
38
+ # rubocop:enable Sorbet/ConstantsFromStrings
39
+ return false unless server_command
40
+
41
+ patch_server_command(server_command)
42
+ true
43
+ end
44
+
45
+ sig { params(server_command: T.untyped).void }
46
+ def self.patch_server_command(server_command)
47
+ return if server_command <= ServerCommandSilencer
48
+
49
+ server_command.prepend(ServerCommandSilencer)
50
+ end
51
+
52
+ module ServerCommandSilencer
53
+ extend T::Sig
54
+
55
+ sig { params(args: T.untyped, block: T.nilable(T.proc.returns(T.untyped))).returns(T.untyped) }
56
+ def perform(*args, &block)
57
+ mark_server_mode!
58
+ super
59
+ end
60
+
61
+ sig { params(server: T.untyped, url: T.nilable(String)).void }
62
+ def print_boot_information(server, url)
63
+ mark_server_mode!
64
+ consume_boot_banner(server, url)
65
+ end
66
+
67
+ private
68
+
69
+ sig { void }
70
+ def mark_server_mode!
71
+ ::LogStruct.instance_variable_set(:@server_mode, true)
72
+ rescue
73
+ # Ignore – server mode is best-effort
74
+ end
75
+
76
+ sig { params(server: T.untyped, url: T.nilable(String)).void }
77
+ def consume_boot_banner(server, url)
78
+ return unless defined?(::LogStruct::Integrations::Puma)
79
+
80
+ begin
81
+ ::LogStruct::Integrations::Puma.emit_boot_if_needed!
82
+ rescue => e
83
+ ::LogStruct::Integrations::Puma.handle_integration_error(e)
84
+ end
85
+
86
+ begin
87
+ model = ::ActiveSupport::Inflector.demodulize(server)
88
+ rescue
89
+ model = "Puma"
90
+ end
91
+
92
+ lines = [
93
+ "=> Booting #{model}",
94
+ build_rails_banner_line(url),
95
+ "=> Run `#{lookup_executable} --help` for more startup options"
96
+ ]
97
+
98
+ lines.each do |line|
99
+ ::LogStruct::Integrations::Puma.process_line(line)
100
+ rescue => e
101
+ ::LogStruct::Integrations::Puma.handle_integration_error(e)
102
+ end
103
+ end
104
+
105
+ sig { params(url: T.nilable(String)).returns(String) }
106
+ def build_rails_banner_line(url)
107
+ suffix = url ? " #{url}" : ""
108
+ "=> Rails #{::Rails.version} application starting in #{::Rails.env}#{suffix}"
109
+ rescue
110
+ "=> Rails application starting"
111
+ end
112
+
113
+ sig { returns(String) }
114
+ def lookup_executable
115
+ return "rails" unless T.unsafe(self).respond_to?(:executable, true)
116
+
117
+ T.cast(T.unsafe(self).send(:executable), String)
118
+ rescue
119
+ "rails"
120
+ end
121
+ end
122
+ end
123
+ end
@@ -5,14 +5,37 @@ require "rails"
5
5
  require "semantic_logger"
6
6
  require_relative "formatter"
7
7
  require_relative "semantic_logger/setup"
8
+ require_relative "integrations"
8
9
 
9
10
  module LogStruct
10
11
  # Railtie to integrate with Rails
11
12
  class Railtie < ::Rails::Railtie
13
+ # Ensure test hosts are allowed early enough for middleware build
14
+ initializer "logstruct.allow_test_hosts", before: :build_middleware_stack do |app|
15
+ if ::Rails.env.test? && app.config.respond_to?(:hosts)
16
+ begin
17
+ app.config.hosts << /.*\z/
18
+ rescue
19
+ # best-effort
20
+ end
21
+ begin
22
+ app.config.middleware.delete(::ActionDispatch::HostAuthorization)
23
+ rescue
24
+ # best-effort
25
+ end
26
+ end
27
+ end
28
+
29
+ # After ActionDispatch is configured, remove HostAuthorization in test to prevent 403s
30
+ # (No late deletion needed; handled above before middleware stack is built)
31
+
12
32
  # Configure early, right after logger initialization
13
33
  initializer "logstruct.configure_logger", after: :initialize_logger do |app|
14
34
  next unless LogStruct.enabled?
15
35
 
36
+ # Apply TaggedLogging monkey patch only when enabled
37
+ require_relative "monkey_patches/active_support/tagged_logging/formatter"
38
+
16
39
  # Use SemanticLogger for powerful logging features
17
40
  LogStruct::SemanticLogger::Setup.configure_semantic_logger(app)
18
41
  end
@@ -26,6 +49,54 @@ module LogStruct
26
49
 
27
50
  # Set up all integrations
28
51
  Integrations.setup_integrations
52
+
53
+ # Note: Host allowances are managed by the test app itself.
29
54
  end
55
+
56
+ # Emit Puma lifecycle logs when running `rails server`
57
+ initializer "logstruct.puma_lifecycle", after: "logstruct.configure_logger" do
58
+ begin
59
+ is_server = ::LogStruct.instance_variable_defined?(:@server_mode) && ::LogStruct.instance_variable_get(:@server_mode)
60
+ rescue
61
+ is_server = false
62
+ end
63
+ next unless is_server
64
+ begin
65
+ require "log_struct/log/puma"
66
+ port = T.let(nil, T.nilable(String))
67
+ ARGV.each_with_index do |arg, idx|
68
+ if arg == "-p" || arg == "--port"
69
+ port = ARGV[idx + 1]
70
+ break
71
+ elsif arg.start_with?("--port=")
72
+ port = arg.split("=", 2)[1]
73
+ break
74
+ end
75
+ end
76
+ started = LogStruct::Log::Puma::Start.new(
77
+ mode: "single",
78
+ environment: (defined?(::Rails) && ::Rails.respond_to?(:env)) ? ::Rails.env : nil,
79
+ process_id: Process.pid,
80
+ listening_addresses: port ? ["tcp://127.0.0.1:#{port}"] : nil
81
+ )
82
+ begin
83
+ warn("[logstruct] puma lifecycle init")
84
+ rescue
85
+ end
86
+ LogStruct.info(started)
87
+
88
+ at_exit do
89
+ shutdown = LogStruct::Log::Puma::Shutdown.new(
90
+ process_id: Process.pid
91
+ )
92
+ LogStruct.info(shutdown)
93
+ end
94
+ rescue
95
+ # best-effort
96
+ end
97
+ end
98
+
99
+ # Delegate integration initializers to Integrations module
100
+ LogStruct::Integrations.setup_initializers(self)
30
101
  end
31
102
  end
@@ -62,7 +62,7 @@ module LogStruct
62
62
  sig { params(log: ::SemanticLogger::Log, logger: T.untyped).returns(String) }
63
63
  def call(log, logger)
64
64
  # Handle LogStruct types specially - they get wrapped in payload hash by SemanticLogger
65
- if log.payload.is_a?(Hash) && log.payload[:payload].is_a?(LogStruct::Log::Interfaces::CommonFields)
65
+ json = if log.payload.is_a?(Hash) && log.payload[:payload].is_a?(LogStruct::Log::Interfaces::CommonFields)
66
66
  # Use our formatter to process LogStruct types
67
67
  @logstruct_formatter.call(log.level, log.time, log.name, log.payload[:payload])
68
68
  elsif log.payload.is_a?(LogStruct::Log::Interfaces::CommonFields)
@@ -77,12 +77,14 @@ module LogStruct
77
77
  else
78
78
  # For plain messages, create a Plain log entry
79
79
  message_data = log.payload || log.message
80
- plain_log = LogStruct::Log::Plain.new(
80
+ plain_log = ::LogStruct::Log::Plain.new(
81
81
  message: message_data,
82
82
  timestamp: log.time
83
83
  )
84
84
  @logstruct_formatter.call(log.level, log.time, log.name, plain_log)
85
85
  end
86
+ # SemanticLogger appenders typically add their own newline. Avoid double newlines by stripping ours.
87
+ json.end_with?("\n") ? json.chomp : json
86
88
  end
87
89
 
88
90
  private
@@ -106,7 +106,7 @@ module LogStruct
106
106
  elsif Rails.env.production?
107
107
  :info
108
108
  elsif Rails.env.test?
109
- :warn
109
+ :debug
110
110
  else
111
111
  :debug
112
112
  end
@@ -119,17 +119,32 @@ module LogStruct
119
119
  # Determine output destination
120
120
  io = determine_output(app)
121
121
 
122
- if Rails.env.development? && config.integrations.enable_color_output
123
- # Use our colorized LogStruct formatter for development
124
- ::SemanticLogger.add_appender(
125
- io: io,
126
- formatter: LogStruct::SemanticLogger::ColorFormatter.new(
127
- color_map: config.integrations.color_map
128
- ),
129
- filter: determine_filter
130
- )
122
+ if Rails.env.development?
123
+ if config.prefer_json_in_development
124
+ # Default to production-style JSON in development when enabled
125
+ ::SemanticLogger.add_appender(
126
+ io: io,
127
+ formatter: LogStruct::SemanticLogger::Formatter.new,
128
+ filter: determine_filter
129
+ )
130
+ elsif config.enable_color_output
131
+ # Opt-in colorful human formatter in development
132
+ ::SemanticLogger.add_appender(
133
+ io: io,
134
+ formatter: LogStruct::SemanticLogger::ColorFormatter.new(
135
+ color_map: config.color_map
136
+ ),
137
+ filter: determine_filter
138
+ )
139
+ else
140
+ ::SemanticLogger.add_appender(
141
+ io: io,
142
+ formatter: LogStruct::SemanticLogger::Formatter.new,
143
+ filter: determine_filter
144
+ )
145
+ end
131
146
  else
132
- # Use our custom JSON formatter
147
+ # Use our custom JSON formatter in non-development environments
133
148
  ::SemanticLogger.add_appender(
134
149
  io: io,
135
150
  formatter: LogStruct::SemanticLogger::Formatter.new,
@@ -147,15 +162,16 @@ module LogStruct
147
162
  end
148
163
  end
149
164
 
150
- sig { params(app: T.untyped).returns(T.any(IO, StringIO)) }
165
+ sig { params(app: T.untyped).returns(T.untyped) }
151
166
  def self.determine_output(app)
152
- if ENV["RAILS_LOG_TO_STDOUT"].present?
153
- $stdout
154
- elsif Rails.env.test?
155
- # Use StringIO for tests to avoid cluttering test output
167
+ # Always honor explicit STDOUT directive, even in test, or when LogStruct is enabled via env
168
+ return $stdout if ENV["RAILS_LOG_TO_STDOUT"].present? || ENV["LOGSTRUCT_ENABLED"].to_s.strip.downcase == "true"
169
+
170
+ if Rails.env.test?
171
+ # Default to StringIO to keep test output clean unless STDOUT is requested
156
172
  StringIO.new
157
173
  else
158
- # Prefer file logging when not explicitly configured for STDOUT
174
+ # Default to STDOUT for app logs
159
175
  $stdout
160
176
  end
161
177
  end
@@ -164,7 +180,7 @@ module LogStruct
164
180
  def self.determine_filter
165
181
  # Filter out noisy loggers if configured
166
182
  config = LogStruct.config
167
- return nil unless config.integrations.filter_noisy_loggers
183
+ return nil unless config.filter_noisy_loggers
168
184
 
169
185
  # Common noisy loggers to filter
170
186
  /\A(ActionView|ActionController::RoutingError|ActiveRecord::SchemaMigration)/
@@ -0,0 +1,22 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # Moved from lib/log_struct/log/interfaces/additional_data_field.rb
5
+ module LogStruct
6
+ module Log
7
+ module Interfaces
8
+ module AdditionalDataField
9
+ extend T::Sig
10
+ extend T::Helpers
11
+
12
+ interface!
13
+
14
+ requires_ancestor { T::Struct }
15
+
16
+ sig { abstract.returns(T.nilable(T::Hash[T.any(String, Symbol), T.untyped])) }
17
+ def additional_data
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,39 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../../enums/source"
5
+ require_relative "../../enums/event"
6
+ require_relative "../../enums/level"
7
+
8
+ module LogStruct
9
+ module Log
10
+ module Interfaces
11
+ module CommonFields
12
+ extend T::Sig
13
+ extend T::Helpers
14
+
15
+ interface!
16
+
17
+ sig { abstract.returns(Source) }
18
+ def source
19
+ end
20
+
21
+ sig { abstract.returns(Event) }
22
+ def event
23
+ end
24
+
25
+ sig { abstract.returns(Level) }
26
+ def level
27
+ end
28
+
29
+ sig { abstract.returns(Time) }
30
+ def timestamp
31
+ end
32
+
33
+ sig { abstract.params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
34
+ def serialize(strict = true)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,29 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../../enums/level"
5
+
6
+ module LogStruct
7
+ module Log
8
+ module Interfaces
9
+ module PublicCommonFields
10
+ extend T::Sig
11
+ extend T::Helpers
12
+
13
+ interface!
14
+
15
+ sig { abstract.returns(Level) }
16
+ def level
17
+ end
18
+
19
+ sig { abstract.returns(Time) }
20
+ def timestamp
21
+ end
22
+
23
+ sig { abstract.params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
24
+ def serialize(strict = true)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end