logstruct 0.1.6 → 0.1.8

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39ad690f2ccb74a697feebdd40978e5cc99fd0c78d4ae550611c01d18aeb56fd
4
- data.tar.gz: 372f2cceb00b8def2054194161f234b8bd193005249c28490442cc0dc59de370
3
+ metadata.gz: 3634f56776f895f97747c8165065651aca55098520469624f73f2d75b090f33c
4
+ data.tar.gz: c29661ad9e8044bad356a5a699f9aaed053c55ec124226784c60dd78655e794f
5
5
  SHA512:
6
- metadata.gz: a8e626ed0af7de549e6aa9a1904421bad0605613d11810554a0defe015730f6bc6e28e2563b92b6460a33fc9d4203d7041727e2007a76218b844a8fce4724709
7
- data.tar.gz: 6b624235b3b53017a2721a37871e215e63fd38aa1d71591679e3976fe8fd26279f404cc37552908eaf52a3fb7b20ca4e872bed7f51c44a8dd10cd4cc41c0f806
6
+ metadata.gz: c491c3087d5341e5ab88bc607c2596be06fb90c482b74498b5aebd5a0eb5ef92f917b889b3ea26f56cb5a69be756bbc0afabfb957bff557cbbd7615f45b3934b
7
+ data.tar.gz: '090f85673e396225d96254c14fdfcdc941904354d3c11700be60b93e83e7c2e230bcbc15c8436ba2e0491223c77e0498f33d866beddb1e34379b7e4863916d1b'
data/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ### Changed
9
9
 
10
+ ## [0.1.8] - 2026-01-22
11
+
12
+ - **Fix**: Lograge custom options now appear in request logs
13
+ - **Fix**: Request logs include request metadata fields (request_id, source_ip, user_agent, referer, host, content_type, accept)
14
+ - **Docs**: Documented Lograge custom options and request metadata fields
15
+
16
+ ## [0.1.7] - 2025-12-06
17
+
18
+ - **Fix**: Puma server detection now uses `$PROGRAM_NAME` instead of checking `defined?(::Puma::Server)` which was unreliable
19
+ - **Fix**: Test isolation for `server_mode` state in configuration tests
20
+ - **CI**: Updated to Ruby 3.4.7 and Rails 8.1.1
21
+
10
22
  ## [0.1.6] - 2025-11-30
11
23
 
12
24
  - Rename `PROVIDER_PUSH_TOKEN` secret to `TF_PROVIDER_GITHUB_TOKEN`
data/README.md CHANGED
@@ -52,6 +52,7 @@ Once initialized (and enabled), the gem automatically includes its modules into
52
52
  - `ActiveSupport::TaggedLogging` is patched to support both Hashes and Strings (only when LogStruct is enabled)
53
53
  - `ActionMailer::Base` includes error handling and event logging modules
54
54
  - We configure `Lograge` for request logging
55
+ - Lograge request logs include request metadata (request_id, source_ip, user_agent, referer, host, content_type, accept) and custom fields from `lograge_custom_options`
55
56
  - A Rack middleware is inserted to catch and log errors, including security violations (IP spoofing, CSRF, blocked hosts, etc.)
56
57
  - Structured logging is set up for ActiveJob, Sidekiq, Shrine, etc.
57
58
  - Rails `config.filter_parameters` are merged into LogStruct's filters and then cleared (to avoid double filtering). Configure sensitive keys via `LogStruct.config.filters`.
@@ -10,7 +10,6 @@ module LogStruct
10
10
  module ClassMethods
11
11
  extend T::Sig
12
12
 
13
- SERVER_COMMAND_ARGS = T.let(["server", "s"].freeze, T::Array[String])
14
13
  CONSOLE_COMMAND_ARGS = T.let(["console", "c"].freeze, T::Array[String])
15
14
  EMPTY_ARGV = T.let([].freeze, T::Array[String])
16
15
  CI_FALSE_VALUES = T.let(["false", "0", "no"].freeze, T::Array[String])
@@ -133,8 +132,30 @@ module LogStruct
133
132
  sig { returns(T::Boolean) }
134
133
  def server_process?
135
134
  return true if logstruct_server_mode?
135
+ return true if puma_server?
136
+ return true if defined?(::Unicorn::HttpServer)
137
+ return true if defined?(::Thin::Server)
138
+ return true if defined?(::Falcon::Server)
139
+ return true if defined?(::Rails::Server)
140
+ return true if sidekiq_server?
141
+
142
+ false
143
+ end
136
144
 
137
- current_argv.any? { |arg| SERVER_COMMAND_ARGS.include?(arg) }
145
+ sig { returns(T::Boolean) }
146
+ def puma_server?
147
+ # Just checking defined?(::Puma::Server) is not reliable - Puma might be installed
148
+ # but not running. Check $PROGRAM_NAME and ARGV to verify we're actually running puma.
149
+ # ARGV check is needed when running through wrapper scripts like gosu.
150
+ return true if $PROGRAM_NAME.include?("puma")
151
+ return true if current_argv.any? { |arg| arg.include?("puma") }
152
+
153
+ false
154
+ end
155
+
156
+ sig { returns(T::Boolean) }
157
+ def sidekiq_server?
158
+ !!(defined?(::Sidekiq) && ::Sidekiq.respond_to?(:server?) && ::Sidekiq.server?)
138
159
  end
139
160
 
140
161
  sig { returns(T::Boolean) }
@@ -33,6 +33,9 @@ module LogStruct
33
33
  UserAgent = new(:user_agent)
34
34
  Referer = new(:referer)
35
35
  RequestId = new(:request_id)
36
+ Host = new(:host)
37
+ ContentType = new(:content_type)
38
+ Accept = new(:accept)
36
39
 
37
40
  # HTTP-specific fields
38
41
  Format = new(:format)
@@ -13,6 +13,29 @@ module LogStruct
13
13
  module Lograge
14
14
  extend IntegrationInterface
15
15
 
16
+ LOGRAGE_KNOWN_KEYS = T.let(
17
+ [
18
+ :method,
19
+ :path,
20
+ :format,
21
+ :controller,
22
+ :action,
23
+ :status,
24
+ :duration,
25
+ :view,
26
+ :db,
27
+ :params,
28
+ :request_id,
29
+ :source_ip,
30
+ :user_agent,
31
+ :referer,
32
+ :host,
33
+ :content_type,
34
+ :accept
35
+ ].freeze,
36
+ T::Array[Symbol]
37
+ )
38
+
16
39
  class << self
17
40
  extend T::Sig
18
41
 
@@ -38,30 +61,9 @@ module LogStruct
38
61
  # The struct is converted to JSON by our Formatter (after filtering, etc.)
39
62
  config.lograge.formatter = T.let(
40
63
  lambda do |data|
41
- # Coerce common fields to expected types
42
- status = ((s = data[:status]) && s.respond_to?(:to_i)) ? s.to_i : s
43
- duration_ms = ((d = data[:duration]) && d.respond_to?(:to_f)) ? d.to_f : d
44
- view = ((v = data[:view]) && v.respond_to?(:to_f)) ? v.to_f : v
45
- db = ((b = data[:db]) && b.respond_to?(:to_f)) ? b.to_f : b
46
-
47
- params = data[:params]
48
- params = params.deep_symbolize_keys if params&.respond_to?(:deep_symbolize_keys)
49
-
50
- Log::Request.new(
51
- http_method: data[:method]&.to_s,
52
- path: data[:path]&.to_s,
53
- format: data[:format]&.to_sym,
54
- controller: data[:controller]&.to_s,
55
- action: data[:action]&.to_s,
56
- status: status,
57
- duration_ms: duration_ms,
58
- view: view,
59
- database: db,
60
- params: params,
61
- timestamp: Time.now
62
- )
64
+ LogStruct::Integrations::Lograge.build_request_log(data)
63
65
  end,
64
- T.proc.params(hash: T::Hash[Symbol, T.untyped]).returns(Log::Request)
66
+ T.proc.params(hash: T::Hash[T.any(Symbol, String), T.untyped]).returns(Log::Request)
65
67
  )
66
68
 
67
69
  # Add custom options to lograge
@@ -100,6 +102,7 @@ module LogStruct
100
102
  return if headers.blank?
101
103
 
102
104
  options[:user_agent] = headers["HTTP_USER_AGENT"]
105
+ options[:referer] = headers["HTTP_REFERER"]
103
106
  options[:content_type] = headers["CONTENT_TYPE"]
104
107
  options[:accept] = headers["HTTP_ACCEPT"]
105
108
  end
@@ -114,6 +117,66 @@ module LogStruct
114
117
  # The proc can modify the options hash directly
115
118
  custom_options_proc.call(event, options)
116
119
  end
120
+
121
+ sig { params(data: T::Hash[T.any(Symbol, String), T.untyped]).returns(Log::Request) }
122
+ def build_request_log(data)
123
+ normalized_data = normalize_lograge_data(data)
124
+
125
+ # Coerce common fields to expected types
126
+ status = ((s = normalized_data[:status]) && s.respond_to?(:to_i)) ? s.to_i : s
127
+ duration_ms = ((d = normalized_data[:duration]) && d.respond_to?(:to_f)) ? d.to_f : d
128
+ view = ((v = normalized_data[:view]) && v.respond_to?(:to_f)) ? v.to_f : v
129
+ db = ((b = normalized_data[:db]) && b.respond_to?(:to_f)) ? b.to_f : b
130
+
131
+ params = normalized_data[:params]
132
+ params = params.deep_symbolize_keys if params&.respond_to?(:deep_symbolize_keys)
133
+
134
+ additional_data = extract_additional_data(normalized_data)
135
+
136
+ Log::Request.new(
137
+ http_method: normalized_data[:method]&.to_s,
138
+ path: normalized_data[:path]&.to_s,
139
+ format: normalized_data[:format]&.to_sym,
140
+ controller: normalized_data[:controller]&.to_s,
141
+ action: normalized_data[:action]&.to_s,
142
+ status: status,
143
+ duration_ms: duration_ms,
144
+ view: view,
145
+ database: db,
146
+ params: params,
147
+ request_id: normalized_data[:request_id]&.to_s,
148
+ source_ip: normalized_data[:source_ip]&.to_s,
149
+ user_agent: normalized_data[:user_agent]&.to_s,
150
+ referer: normalized_data[:referer]&.to_s,
151
+ host: normalized_data[:host]&.to_s,
152
+ content_type: normalized_data[:content_type]&.to_s,
153
+ accept: normalized_data[:accept]&.to_s,
154
+ additional_data: additional_data,
155
+ timestamp: Time.now
156
+ )
157
+ end
158
+
159
+ sig { params(data: T::Hash[T.any(Symbol, String), T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
160
+ def normalize_lograge_data(data)
161
+ data.each_with_object({}) do |(key, value), normalized|
162
+ normalized[key.to_s.to_sym] = value
163
+ end
164
+ end
165
+
166
+ sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
167
+ def extract_additional_data(data)
168
+ extras = T.let({}, T::Hash[Symbol, T.untyped])
169
+ data.each do |key, value|
170
+ next if LOGRAGE_KNOWN_KEYS.include?(key)
171
+ next if value.nil?
172
+
173
+ extras[key] = value
174
+ end
175
+
176
+ return nil if extras.empty?
177
+
178
+ extras
179
+ end
117
180
  end
118
181
  end
119
182
  end
@@ -14,6 +14,8 @@ module LogStruct
14
14
  extend T::Sig
15
15
  extend IntegrationInterface
16
16
 
17
+ SHRINE_EVENTS = T.let(%i[upload exists download delete metadata open].freeze, T::Array[Symbol])
18
+
17
19
  # Set up Shrine structured logging
18
20
  sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
19
21
  def self.setup(config)
@@ -30,9 +32,10 @@ module LogStruct
30
32
  event_type = case event.name
31
33
  when :upload then Event::Upload
32
34
  when :download then Event::Download
35
+ when :open then Event::Download
33
36
  when :delete then Event::Delete
34
37
  when :metadata then Event::Metadata
35
- when :exists then Event::Exist # ActiveStorage uses 'exist', may as well use that
38
+ when :exists then Event::Exist
36
39
  else Event::Unknown
37
40
  end
38
41
 
@@ -80,18 +83,78 @@ module LogStruct
80
83
  Log::Shrine::Metadata.new(**unknown_params)
81
84
  end
82
85
 
83
- # Pass the structured hash to the logger
84
- # If Rails.logger has our Formatter, it will handle JSON conversion
85
- ::Shrine.logger.info log_data
86
+ # Log directly through SemanticLogger, NOT through Shrine.logger
87
+ # Shrine.logger is a basic Logger that would just call .to_s on the struct
88
+ ::SemanticLogger[::Shrine].info(log_data)
86
89
  end)
87
90
 
88
- # Configure Shrine to use our structured log subscriber
89
- ::Shrine.plugin :instrumentation,
90
- events: %i[upload exists download delete],
91
- log_subscriber: shrine_log_subscriber
91
+ # Check if instrumentation plugin is already loaded
92
+ # If so, we need to replace the existing subscribers, not add duplicates
93
+ if instrumentation_already_configured?
94
+ replace_existing_subscribers(shrine_log_subscriber)
95
+ else
96
+ # First time setup - configure the instrumentation plugin
97
+ ::Shrine.plugin :instrumentation,
98
+ events: SHRINE_EVENTS,
99
+ log_subscriber: shrine_log_subscriber
100
+ end
92
101
 
93
102
  true
94
103
  end
104
+
105
+ sig { returns(T::Boolean) }
106
+ def self.instrumentation_already_configured?
107
+ return false unless defined?(::Shrine)
108
+
109
+ opts = T.unsafe(::Shrine).opts
110
+ return false unless opts.is_a?(Hash)
111
+
112
+ instrumentation_opts = opts[:instrumentation]
113
+ return false unless instrumentation_opts.is_a?(Hash)
114
+
115
+ subscribers = instrumentation_opts[:subscribers]
116
+ return false unless subscribers.is_a?(Hash)
117
+
118
+ !subscribers.empty?
119
+ end
120
+
121
+ sig { params(new_subscriber: T.untyped).void }
122
+ def self.replace_existing_subscribers(new_subscriber)
123
+ opts = T.unsafe(::Shrine).opts
124
+ instrumentation_opts = opts[:instrumentation]
125
+ subscribers = instrumentation_opts[:subscribers]
126
+
127
+ # Clear all existing subscribers and add our new one
128
+ SHRINE_EVENTS.each do |event_name|
129
+ # Clear existing subscribers for this event
130
+ subscribers[event_name] = [] if subscribers[event_name]
131
+
132
+ # Add our subscriber
133
+ subscribers[event_name] ||= []
134
+ subscribers[event_name] << new_subscriber
135
+
136
+ # Also re-subscribe via ActiveSupport::Notifications
137
+ # Shrine uses "shrine.#{event_name}" as the notification name
138
+ notification_name = "shrine.#{event_name}"
139
+
140
+ # Unsubscribe existing listeners for this event
141
+ # ActiveSupport::Notifications stores subscriptions, we need to find and remove them
142
+ notifier = ::ActiveSupport::Notifications.notifier
143
+ if notifier.respond_to?(:listeners_for)
144
+ # Rails 7.0+ uses listeners_for
145
+ listeners = notifier.listeners_for(notification_name)
146
+ listeners.each do |listener|
147
+ ::ActiveSupport::Notifications.unsubscribe(listener)
148
+ end
149
+ end
150
+
151
+ # Subscribe our new subscriber
152
+ ::ActiveSupport::Notifications.subscribe(notification_name) do |*args|
153
+ event = ::ActiveSupport::Notifications::Event.new(*args)
154
+ new_subscriber.call(event)
155
+ end
156
+ end
157
+ end
95
158
  end
96
159
  end
97
160
  end
@@ -44,6 +44,14 @@ module LogStruct
44
44
  const :view, T.nilable(Float), default: nil
45
45
  const :database, T.nilable(Float), default: nil
46
46
  const :params, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
47
+ const :host, T.nilable(String), default: nil
48
+ const :content_type, T.nilable(String), default: nil
49
+ const :accept, T.nilable(String), default: nil
50
+
51
+ # Additional data
52
+ include LogStruct::Log::Interfaces::AdditionalDataField
53
+ const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
54
+ include LogStruct::Log::Shared::MergeAdditionalDataFields
47
55
 
48
56
  # Request fields (optional)
49
57
  include LogStruct::Log::Interfaces::RequestFields
@@ -70,6 +78,9 @@ module LogStruct
70
78
  h[LogField::View] = view unless view.nil?
71
79
  h[LogField::Database] = database unless database.nil?
72
80
  h[LogField::Params] = params unless params.nil?
81
+ h[LogField::Host] = host unless host.nil?
82
+ h[LogField::ContentType] = content_type unless content_type.nil?
83
+ h[LogField::Accept] = accept unless accept.nil?
73
84
  h
74
85
  end
75
86
  end
@@ -3,16 +3,19 @@
3
3
 
4
4
  require "active_support/tagged_logging"
5
5
 
6
- # Monkey-patch ActiveSupport::TaggedLogging::Formatter to support hash inputs
7
- # This allows us to pass structured data to the logger and have tags incorporated
8
- # directly into the hash instead of being prepended as strings
6
+ # Monkey-patch ActiveSupport::TaggedLogging::Formatter to work with structured logging.
7
+ #
8
+ # Problem: Rails' TaggedLogging prepends tags as text and converts messages to strings.
9
+ # When we pass a hash to super(), Rails does "#{tags_text}#{msg}" which calls .to_s
10
+ # on the hash, producing Ruby inspect format {message: "..."} instead of valid JSON.
11
+ #
12
+ # Solution: When LogStruct is enabled, we add tags as a hash entry and delegate to
13
+ # LogStruct::Formatter for proper JSON serialization with filtering and standard fields.
9
14
  module ActiveSupport
10
15
  module TaggedLogging
11
16
  extend T::Sig
12
17
 
13
18
  # Add class-level current_tags method for compatibility with Rails code
14
- # that expects to call ActiveSupport::TaggedLogging.current_tags
15
- # Use thread-local storage directly like Rails does internally
16
19
  sig { returns(T::Array[T.any(String, Symbol)]) }
17
20
  def self.current_tags
18
21
  Thread.current[:activesupport_tagged_logging_tags] || []
@@ -23,29 +26,33 @@ module ActiveSupport
23
26
  extend T::Helpers
24
27
  requires_ancestor { ::ActiveSupport::TaggedLogging::Formatter }
25
28
 
26
- # Override the call method to support hash input/output, and wrap
27
- # plain strings in a Hash under a `msg` key.
28
- # The data is then passed to our custom log formatter that transforms it
29
- # into a JSON string before logging.
30
- #
31
- # IMPORTANT: This only applies when LogStruct is enabled. When disabled,
32
- # we preserve the original Rails logging behavior to avoid wrapping
33
- # messages in hashes (which would break default Rails log formatting).
34
29
  sig { params(severity: T.any(String, Symbol), time: Time, progname: T.untyped, data: T.untyped).returns(String) }
35
30
  def call(severity, time, progname, data)
36
- # Skip hash wrapping when LogStruct is disabled to preserve default Rails behavior
31
+ # Preserve original Rails behavior when LogStruct is disabled
37
32
  return super unless ::LogStruct.enabled?
38
33
 
39
- # Convert data to a hash if it's not already one
40
- data = {message: data.to_s} unless data.is_a?(Hash)
41
-
42
- # Add current tags to the hash if present
43
- # Use thread-local storage directly as fallback if current_tags method doesn't exist
34
+ # Get current tags
44
35
  tags = T.unsafe(self).respond_to?(:current_tags) ? current_tags : (Thread.current[:activesupport_tagged_logging_tags] || [])
45
- data[:tags] = tags if tags.present?
46
36
 
47
- # Call the original formatter with our enhanced data
48
- super
37
+ # Add tags to data as hash entry (not text prefix)
38
+ data_with_tags = if data.is_a?(Hash)
39
+ tags.present? ? data.merge(tags: tags) : data
40
+ elsif data.is_a?(::LogStruct::Log::Interfaces::CommonFields) || (data.is_a?(T::Struct) && data.respond_to?(:serialize))
41
+ hash = T.unsafe(data).serialize
42
+ tags.present? ? hash.merge(tags: tags) : hash
43
+ else
44
+ tags.present? ? {message: data.to_s, tags: tags} : {message: data.to_s}
45
+ end
46
+
47
+ # Delegate to LogStruct::Formatter for JSON serialization with filtering
48
+ logstruct_formatter.call(severity, time, progname, data_with_tags)
49
+ end
50
+
51
+ private
52
+
53
+ sig { returns(::LogStruct::Formatter) }
54
+ def logstruct_formatter
55
+ @logstruct_formatter ||= T.let(::LogStruct::Formatter.new, T.nilable(::LogStruct::Formatter))
49
56
  end
50
57
  end
51
58
  end
@@ -61,16 +61,12 @@ module LogStruct
61
61
 
62
62
  sig { params(log: ::SemanticLogger::Log, logger: T.untyped).returns(String) }
63
63
  def call(log, logger)
64
- # Handle LogStruct types specially - they get wrapped in payload hash by SemanticLogger
65
- json = if log.payload.is_a?(Hash) && log.payload[:payload].is_a?(LogStruct::Log::Interfaces::CommonFields)
66
- # Use our formatter to process LogStruct types
67
- @logstruct_formatter.call(log.level, log.time, log.name, log.payload[:payload])
68
- elsif log.payload.is_a?(LogStruct::Log::Interfaces::CommonFields)
69
- # Direct LogStruct (fallback case)
70
- @logstruct_formatter.call(log.level, log.time, log.name, log.payload)
71
- elsif log.payload.is_a?(Hash) && log.payload[:payload].is_a?(T::Struct)
72
- # T::Struct wrapped in payload hash
73
- @logstruct_formatter.call(log.level, log.time, log.name, log.payload[:payload])
64
+ # Extract LogStruct from various locations where it might be stored
65
+ logstruct = extract_logstruct(log)
66
+
67
+ json = if logstruct
68
+ # Use our formatter to process LogStruct types directly
69
+ @logstruct_formatter.call(log.level, log.time, log.name, logstruct)
74
70
  elsif log.payload.is_a?(Hash) || log.payload.is_a?(T::Struct)
75
71
  # Process hashes and T::Structs through our formatter
76
72
  @logstruct_formatter.call(log.level, log.time, log.name, log.payload)
@@ -89,6 +85,48 @@ module LogStruct
89
85
 
90
86
  private
91
87
 
88
+ # Extract a LogStruct from the various places it might be stored in a SemanticLogger::Log
89
+ sig do
90
+ params(log: ::SemanticLogger::Log).returns(
91
+ T.nilable(
92
+ T.any(
93
+ LogStruct::Log::Interfaces::CommonFields,
94
+ LogStruct::Log::Interfaces::PublicCommonFields,
95
+ T::Struct
96
+ )
97
+ )
98
+ )
99
+ end
100
+ def extract_logstruct(log)
101
+ # Check payload first (most common path for structured logging)
102
+ if log.payload.is_a?(Hash) && log.payload[:payload].is_a?(LogStruct::Log::Interfaces::CommonFields)
103
+ return T.cast(log.payload[:payload], LogStruct::Log::Interfaces::CommonFields)
104
+ end
105
+
106
+ if log.payload.is_a?(LogStruct::Log::Interfaces::CommonFields)
107
+ return log.payload
108
+ end
109
+
110
+ if log.payload.is_a?(LogStruct::Log::Interfaces::PublicCommonFields)
111
+ return log.payload
112
+ end
113
+
114
+ # Check message - this is where structs end up when passed directly to logger.info(struct)
115
+ if log.message.is_a?(LogStruct::Log::Interfaces::CommonFields)
116
+ return T.cast(log.message, LogStruct::Log::Interfaces::CommonFields)
117
+ end
118
+
119
+ # Check for T::Struct in payload hash (might be a LogStruct struct not implementing CommonFields directly)
120
+ if log.payload.is_a?(Hash) && log.payload[:payload].is_a?(T::Struct)
121
+ struct = log.payload[:payload]
122
+ if struct.respond_to?(:source) && struct.respond_to?(:event)
123
+ return struct
124
+ end
125
+ end
126
+
127
+ nil
128
+ end
129
+
92
130
  sig { returns(LogStruct::Formatter) }
93
131
  attr_reader :logstruct_formatter
94
132
  end
@@ -101,7 +101,7 @@ module LogStruct
101
101
 
102
102
  sig { params(app: T.untyped).returns(Symbol) }
103
103
  def self.determine_log_level(app)
104
- if app.config.log_level
104
+ level = if app.config.log_level
105
105
  app.config.log_level
106
106
  elsif Rails.env.production?
107
107
  :info
@@ -110,6 +110,8 @@ module LogStruct
110
110
  else
111
111
  :debug
112
112
  end
113
+ # Rails config.log_level can be a String or Symbol
114
+ level.is_a?(String) ? level.to_sym : level
113
115
  end
114
116
 
115
117
  sig { params(app: T.untyped).void }
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module LogStruct
5
- VERSION = "0.1.6"
5
+ VERSION = "0.1.8"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstruct
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - DocSpring