lumberjack_rails 1.0.0

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.
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Integration patches for Lumberjack Rails support.
4
+ #
5
+ # This file applies various patches and extensions to integrate Lumberjack
6
+ # with Rails framework components. It performs the following key integrations:
7
+ #
8
+ # 1. Removes deprecated Lumberjack::Logger methods that conflict with ActiveSupport
9
+ # 2. Adds tagged logging support to Lumberjack formatters and loggers
10
+ # 3. Extends ActiveSupport::BroadcastLogger with Lumberjack compatibility
11
+ # 4. Integrates with Rails framework components (ActiveJob, ActionCable, etc.)
12
+ # 5. Sets up log subscribers to use forked Lumberjack loggers
13
+ #
14
+ # These patches ensure seamless integration between Lumberjack's logging
15
+ # capabilities and Rails' existing logging infrastructure while maintaining
16
+ # backward compatibility with existing Rails logging patterns.
17
+
18
+ # Remove deprecated methods on Lumberjack::Logger that are implemented by ActiveSupport
19
+ Lumberjack::Logger.remove_method(:tagged) if Lumberjack::Logger.instance_methods.include?(:tagged)
20
+ Lumberjack::Logger.remove_method(:log_at) if Lumberjack::Logger.instance_methods.include?(:log_at)
21
+ Lumberjack::Logger.remove_method(:silence) if Lumberjack::Logger.instance_methods.include?(:silence)
22
+
23
+ # Add tagged logging support to the Lumberjack::EntryFormatter
24
+ Lumberjack::EntryFormatter.prepend(Lumberjack::Rails::TaggedLoggingFormatter)
25
+ Lumberjack::EntryFormatter.include(ActiveSupport::TaggedLogging::Formatter)
26
+
27
+ # Add silence method to Lumberjack::Logger
28
+ require "active_support/logger_silence"
29
+ Lumberjack::ContextLogger.include(ActiveSupport::LoggerSilence)
30
+
31
+ # Use prepend to ensure level is overridden properly
32
+ Lumberjack::ContextLogger.prepend(Lumberjack::Rails::LogAtLevel)
33
+
34
+ # Add tagged logging support to Lumberjack
35
+ Lumberjack::ContextLogger.prepend(ActiveSupport::TaggedLogging)
36
+ Lumberjack::ForkedLogger.include(Lumberjack::Rails::TaggedForkedLogger)
37
+
38
+ ActiveSupport::BroadcastLogger.prepend(Lumberjack::Rails::BroadcastLoggerExtension)
39
+
40
+ ActiveSupport.on_load(:active_job) do
41
+ ActiveJob::Base.prepend(Lumberjack::Rails::ActiveJobExtension)
42
+ ActiveJob::LogSubscriber.prepend(Lumberjack::Rails::LogSubscriberExtension)
43
+ ActiveJob::LogSubscriber.logger = -> { ActiveJob::Base.logger }
44
+ end
45
+
46
+ ActiveSupport.on_load(:action_cable) do
47
+ ActionCable::Connection::Base.prepend(Lumberjack::Rails::ActionCableExtension)
48
+ end
49
+
50
+ ActiveSupport.on_load(:action_mailer) do
51
+ ActionMailer::Base.prepend(Lumberjack::Rails::ActionMailerExtension)
52
+ ActionMailer::LogSubscriber.prepend(Lumberjack::Rails::LogSubscriberExtension)
53
+ ActionMailer::LogSubscriber.logger = -> { ActionMailer::Base.logger }
54
+ end
55
+
56
+ ActiveSupport.on_load(:action_mailbox) do
57
+ ActionMailbox::Base.prepend(Lumberjack::Rails::ActionMailboxExtension)
58
+ end
59
+
60
+ ActiveSupport.on_load(:action_controller) do
61
+ # Add hook to allow disabling of "Started ..." log lines in Rack::Logger
62
+ ::Rails::Rack::Logger.prepend(Lumberjack::Rails::RackLoggerExtension) if defined?(::Rails::Rack::Logger)
63
+
64
+ ActionController::Base.prepend(Lumberjack::Rails::ActionControllerExtension)
65
+
66
+ ActionController::LogSubscriber.prepend(Lumberjack::Rails::ActionControllerLogSubscriberExtension)
67
+ ActionController::LogSubscriber.prepend(Lumberjack::Rails::LogSubscriberExtension)
68
+ ActionController::LogSubscriber.logger = -> { ActionController::Base.logger }
69
+
70
+ ActionDispatch::LogSubscriber.prepend(Lumberjack::Rails::LogSubscriberExtension)
71
+ end
72
+
73
+ ActiveSupport.on_load(:action_view) do
74
+ ActionView::LogSubscriber.prepend(Lumberjack::Rails::LogSubscriberExtension)
75
+ ActionView::LogSubscriber.logger = -> { ActionView::Base.logger }
76
+ end
77
+
78
+ ActiveSupport.on_load(:active_record) do
79
+ ActiveRecord::LogSubscriber.prepend(Lumberjack::Rails::LogSubscriberExtension)
80
+ ActiveRecord::LogSubscriber.logger = -> { ActiveRecord::Base.logger }
81
+ end
82
+
83
+ ActiveSupport.on_load(:active_storage_record) do
84
+ ActiveStorage::LogSubscriber.prepend(Lumberjack::Rails::LogSubscriberExtension)
85
+ ActiveStorage::LogSubscriber.logger = -> { ActiveStorage.logger }
86
+ end
@@ -0,0 +1,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ module Rails
5
+ # Extension for ActiveSupport::BroadcastLogger to provide Lumberjack compatibility.
6
+ #
7
+ # This module extends ActiveSupport::BroadcastLogger to ensure proper handling
8
+ # of Lumberjack loggers when broadcasting to multiple loggers, including
9
+ # support for Lumberjack-specific features like attributes and contexts.
10
+ module BroadcastLoggerExtension
11
+ # Initialize the broadcast logger with validation for Lumberjack loggers.
12
+ #
13
+ # @param loggers [Array<Logger>] array of loggers to broadcast to
14
+ # @raise [ArgumentError] if more than one Lumberjack logger is provided
15
+ def initialize(*loggers)
16
+ if loggers.count { |logger| logger.is_a?(Lumberjack::ContextLogger) } > 1
17
+ raise ArgumentError, "Only one Lumberjack logger is allowed"
18
+ end
19
+
20
+ super
21
+ end
22
+
23
+ # Log a debug message with optional attributes support.
24
+ #
25
+ # @param message_or_progname_or_attributes [String, Hash] the message, progname, or attributes
26
+ # @param progname_or_attributes [String, Hash] the progname or attributes
27
+ # @yield optional block that returns the message
28
+ # @return [void]
29
+ def debug(message_or_progname_or_attributes = nil, progname_or_attributes = nil, &block)
30
+ dispatch_to_each(block) do |logger, one_time_block|
31
+ call_with_attributes_arg(logger, :debug, message_or_progname_or_attributes, progname_or_attributes, &one_time_block)
32
+ end
33
+ end
34
+
35
+ # Log an info message with optional attributes support.
36
+ #
37
+ # @param message_or_progname_or_attributes [String, Hash] the message, progname, or attributes
38
+ # @param progname_or_attributes [String, Hash] the progname or attributes
39
+ # @yield optional block that returns the message
40
+ # @return [void]
41
+ def info(message_or_progname_or_attributes = nil, progname_or_attributes = nil, &block)
42
+ dispatch_to_each(block) do |logger, one_time_block|
43
+ call_with_attributes_arg(logger, :info, message_or_progname_or_attributes, progname_or_attributes, &one_time_block)
44
+ end
45
+ end
46
+
47
+ # Log a warning message with optional attributes support.
48
+ #
49
+ # @param message_or_progname_or_attributes [String, Hash] the message, progname, or attributes
50
+ # @param progname_or_attributes [String, Hash] the progname or attributes
51
+ # @yield optional block that returns the message
52
+ # @return [void]
53
+ def warn(message_or_progname_or_attributes = nil, progname_or_attributes = nil, &block)
54
+ dispatch_to_each(block) do |logger, one_time_block|
55
+ call_with_attributes_arg(logger, :warn, message_or_progname_or_attributes, progname_or_attributes, &one_time_block)
56
+ end
57
+ end
58
+
59
+ # Log an error message with optional attributes support.
60
+ #
61
+ # @param message_or_progname_or_attributes [String, Hash] the message, progname, or attributes
62
+ # @param progname_or_attributes [String, Hash] the progname or attributes
63
+ # @yield optional block that returns the message
64
+ # @return [void]
65
+ def error(message_or_progname_or_attributes = nil, progname_or_attributes = nil, &block)
66
+ dispatch_to_each(block) do |logger, one_time_block|
67
+ call_with_attributes_arg(logger, :error, message_or_progname_or_attributes, progname_or_attributes, &one_time_block)
68
+ end
69
+ end
70
+
71
+ # Log a fatal message with optional attributes support.
72
+ #
73
+ # @param message_or_progname_or_attributes [String, Hash] the message, progname, or attributes
74
+ # @param progname_or_attributes [String, Hash] the progname or attributes
75
+ # @yield optional block that returns the message
76
+ # @return [void]
77
+ def fatal(message_or_progname_or_attributes = nil, progname_or_attributes = nil, &block)
78
+ dispatch_to_each(block) do |logger, one_time_block|
79
+ call_with_attributes_arg(logger, :fatal, message_or_progname_or_attributes, progname_or_attributes, &one_time_block)
80
+ end
81
+ end
82
+
83
+ # Log an unknown severity message with optional attributes support.
84
+ #
85
+ # @param message_or_progname_or_attributes [String, Hash] the message, progname, or attributes
86
+ # @param progname_or_attributes [String, Hash] the progname or attributes
87
+ # @yield optional block that returns the message
88
+ # @return [void]
89
+ def unknown(message_or_progname_or_attributes = nil, progname_or_attributes = nil, &block)
90
+ dispatch_to_each(block) do |logger, one_time_block|
91
+ call_with_attributes_arg(logger, :unknown, message_or_progname_or_attributes, progname_or_attributes, &one_time_block)
92
+ end
93
+ end
94
+
95
+ # Add a log entry with the specified severity and optional attributes support.
96
+ #
97
+ # @param severity [Integer, Symbol, String] the severity for the log entry
98
+ # @param message_or_progname_or_attributes [String, Hash] the message, progname, or attributes
99
+ # @param progname_or_attributes [String, Hash] the progname or attributes
100
+ # @yield optional block that returns the message
101
+ # @return [void]
102
+ def add(severity, message_or_progname_or_attributes = nil, progname_or_attributes = nil, &block)
103
+ severity = Logger::Severity.coerce(severity)
104
+ dispatch_to_each(block) do |logger, one_time_block|
105
+ call_add_with_attributes_arg(logger, severity, message_or_progname_or_attributes, progname_or_attributes, &one_time_block)
106
+ end
107
+ end
108
+
109
+ alias_method :log, :add
110
+
111
+ # Override the with_level method defined on the logger gem to use Rails' log_at method instead.
112
+ #
113
+ # @param level [Symbol, Integer] the log level to set temporarily
114
+ # @yield the block to execute at the specified log level
115
+ # @return [Object] the result of the block execution
116
+ def with_level(level, &block)
117
+ log_at(level, &block)
118
+ end
119
+
120
+ # Tag log entries with the specified attributes.
121
+ #
122
+ # @param attributes [Hash] the attributes to tag with
123
+ # @yield the block to execute with the tagged context
124
+ # @return [Object] the result of the block execution
125
+ def tag(attributes, &block)
126
+ dispatch_block_method(:tag, attributes, &block)
127
+ end
128
+
129
+ # Execute a block within a logger context.
130
+ #
131
+ # @yield the block to execute within the context
132
+ # @return [Object] the result of the block execution
133
+ def context(&block)
134
+ dispatch_block_method(:context, &block)
135
+ end
136
+
137
+ # Ensure a Lumberjack context is present for the duration of the block.
138
+ #
139
+ # @yield the block to execute within the ensured context
140
+ # @return [Object] the result of the block execution
141
+ def ensure_context(&block)
142
+ dispatch_block_method(:ensure_context, &block)
143
+ end
144
+
145
+ # Append values to an existing attribute.
146
+ #
147
+ # @param attribute_name [String, Symbol] the name of the attribute
148
+ # @param tag [Array] the values to append
149
+ # @yield the block to execute with the modified attribute
150
+ # @return [Object] the result of the block execution
151
+ def append_to(attribute_name, *tag, &block)
152
+ dispatch_block_method(:append_to, attribute_name, *tag, &block)
153
+ end
154
+
155
+ # Clear all current attributes.
156
+ #
157
+ # @yield the block to execute with cleared attributes
158
+ # @return [Object] the result of the block execution
159
+ def clear_attributes(&block)
160
+ dispatch_block_method(:clear_attributes, &block)
161
+ end
162
+
163
+ # Execute a block without any tags.
164
+ #
165
+ # @yield the block to execute without tags
166
+ # @return [Object] the result of the block execution
167
+ def untagged(&block)
168
+ dispatch_block_method(:untagged, &block)
169
+ end
170
+
171
+ # Set the progname temporarily for a block.
172
+ #
173
+ # @param value [String] the progname to set
174
+ # @yield the block to execute with the specified progname
175
+ # @return [Object] the result of the block execution
176
+ def with_progname(value, &block)
177
+ dispatch_block_method(:with_progname, value, &block)
178
+ end
179
+
180
+ # Alias for with_progname for backward compatibility.
181
+ #
182
+ # @param value [String] the progname to set
183
+ # @yield the block to execute with the specified progname
184
+ # @return [Object] the result of the block execution
185
+ def set_progname(value, &block)
186
+ dispatch_block_method(:with_progname, value, &block)
187
+ end
188
+
189
+ # Create a forked logger from this broadcast logger.
190
+ #
191
+ # @param level [Symbol, Integer] optional log level for the forked logger
192
+ # @param progname [String] optional progname for the forked logger
193
+ # @param attributes [Hash] optional attributes for the forked logger
194
+ # @return [Lumberjack::ForkedLogger] the forked logger
195
+ def fork(level: nil, progname: nil, attributes: nil)
196
+ logger = Lumberjack::ForkedLogger.new(self)
197
+ logger.level = level if level
198
+ logger.progname = progname if progname
199
+ logger.tag!(attributes) if attributes && !attributes.empty?
200
+ logger
201
+ end
202
+
203
+ private
204
+
205
+ # Lumberjack loggers support an optional attributes argument to the logging methods. This method provides
206
+ # compatibility with other Loggers that don't support that argument. The arguments here are funky because
207
+ # the methods on Logger are funky. On Logger the sole argument to a logging method can be message or
208
+ # the progname depending on if the message is in the block.
209
+ #
210
+ # @param logger [Logger] the logger to call the method on
211
+ # @param method [Symbol] the logging method to call
212
+ # @param message_or_progname_or_attributes [String, Hash] the message, progname, or attributes
213
+ # @param progname_or_attributes [String, Hash] the progname or attributes
214
+ # @yield optional block that returns the message
215
+ # @return [void]
216
+ def call_with_attributes_arg(logger, method, message_or_progname_or_attributes, progname_or_attributes, &block)
217
+ if logger.is_a?(Lumberjack::ContextLogger)
218
+ logger.send(method, message_or_progname_or_attributes, progname_or_attributes, &block)
219
+ elsif block
220
+ progname = message_or_progname_or_attributes unless progname_or_attributes.is_a?(Hash)
221
+ logger.send(method, progname, &block)
222
+ else
223
+ logger.send(method, message_or_progname_or_attributes)
224
+ end
225
+ end
226
+
227
+ # Lumberjack loggers support an optional attributes argument to the logging methods. This method provides
228
+ # compatibility with other Loggers that don't support that argument. The arguments here are funky because
229
+ # the methods on Logger#add are funky. On Logger the sole argument to a logging method can be message or
230
+ # the progname depending on if the message is in the block.
231
+ #
232
+ # @param logger [Logger] the logger to call the method on
233
+ # @param severity [Integer, Symbol, String] the severity for the log entry
234
+ # @param message_or_progname_or_attributes [String, Hash] the message, progname, or attributes
235
+ # @param progname_or_attributes [String, Hash] the progname or attributes
236
+ # @yield optional block that returns the message
237
+ # @return [void]
238
+ def call_add_with_attributes_arg(logger, severity, message_or_progname_or_attributes, progname_or_attributes, &block)
239
+ if logger.is_a?(Lumberjack::ContextLogger)
240
+ logger.add(severity, message_or_progname_or_attributes, progname_or_attributes, &block)
241
+ elsif block
242
+ progname = message_or_progname_or_attributes unless progname_or_attributes.is_a?(Hash)
243
+ logger.add(severity, progname, &block)
244
+ else
245
+ logger.add(severity, message_or_progname_or_attributes)
246
+ end
247
+ end
248
+
249
+ # Guard against multiple loggers responding to the same method that takes
250
+ # a block. In that case we don't want to call the block multiple times.
251
+ # This does not include the logging methods like `info` but does include
252
+ # methods like `tag` that are expected to be called with a block that
253
+ # has business logic inside of it.
254
+ #
255
+ # This method is used for methods that modify the state of the logger. Since the block
256
+ # is only called once, other loggers will not have the internal state modified as expected.
257
+ #
258
+ # @param name [Symbol] the method name to dispatch
259
+ # @yield optional block to execute
260
+ # @return [Object] the result of the method execution
261
+ def dispatch_block_method(name, ...)
262
+ loggers = broadcasts.select { |logger| logger.respond_to?(name) }
263
+
264
+ if loggers.none?
265
+ result = yield if block_given?
266
+ return result
267
+ end
268
+
269
+ return loggers.first.send(name, ...) if loggers.one?
270
+
271
+ message = "BroadcastLogger cannot call #{name} on multiple loggers with a block."
272
+ loggers.first.warn("#{message} It was called on this logger.")
273
+ loggers[1..].each { |logger| logger.warn("#{message} It was not called on this logger.") }
274
+ loggers.first.send(name, ...)
275
+ end
276
+
277
+ # Dispatch to each logger and the block if any. The block will only be called once.
278
+ #
279
+ # @param block [Proc] Block that will be called on the wrapped method.
280
+ # @yield block to execute for each logger
281
+ # @return [Object] the result of the first logger's block execution
282
+ # @yieldparam logger [Logger] the current logger
283
+ # @yieldparam one_time_block [Proc, nil] the block to execute, or nil if no block was given.
284
+ def dispatch_to_each(block)
285
+ if block
286
+ called = false
287
+ result = nil
288
+ one_time_block = lambda do
289
+ if called
290
+ result
291
+ else
292
+ called = true
293
+ result = block.call
294
+ end
295
+ end
296
+ end
297
+
298
+ broadcasts.map { |logger| yield(logger, one_time_block) }.first
299
+ end
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ module Rails
5
+ # This Rack middleware provides a Lumberjack context for each request.
6
+ class ContextMiddleware
7
+ # @param app [#call] The Rack application.
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ # Process a request through the middleware stack with Lumberjack context.
13
+ #
14
+ # @param env [Hash] the Rack environment hash
15
+ # @return [Array] the Rack response array
16
+ def call(env)
17
+ Lumberjack::Rails.logger_context do
18
+ @app.call(env)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack
4
+ module Rails
5
+ # Extension that provides log level override functionality.
6
+ #
7
+ # This module allows temporary overriding of the logger's level using
8
+ # Rails' local_level mechanism while maintaining compatibility with
9
+ # the underlying Lumberjack logger.
10
+ module LogAtLevel
11
+ # Get the effective log level, checking for local level override first.
12
+ #
13
+ # @return [Integer] the effective log level
14
+ def level
15
+ local_level || super
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack::Rails
4
+ # Extension for Rails log subscribers to provide Lumberjack logger integration.
5
+ #
6
+ # This module enables Rails log subscribers (such as ActiveRecord::LogSubscriber,
7
+ # ActionController::LogSubscriber, etc.) to use forked Lumberjack loggers.
8
+ # Forked loggers provide isolation, allowing different log levels and attributes
9
+ # to be set for different log subscribers without affecting the main logger.
10
+ #
11
+ # When a log subscriber's logger is accessed, this extension checks if the
12
+ # parent logger supports forking. If it does, a ForkedLogger is created and
13
+ # cached for subsequent use. This ensures each log subscriber has its own
14
+ # isolated logger context.
15
+ module LogSubscriberExtension
16
+ extend ActiveSupport::Concern
17
+
18
+ prepended do
19
+ @__logger ||= nil
20
+ @__silenced_events ||= nil
21
+ end
22
+
23
+ class_methods do
24
+ # Get the logger for this log subscriber class.
25
+ #
26
+ # This method overrides the default Rails log subscriber logger behavior
27
+ # to provide Lumberjack forked logger support. If the parent logger supports
28
+ # forking, a ForkedLogger is created and cached. Otherwise, the original
29
+ # logger is returned unchanged.
30
+ #
31
+ # @return [Lumberjack::ForkedLogger, Logger] the logger instance for this subscriber
32
+ def logger
33
+ class_logger = super
34
+ class_logger = class_logger.call if class_logger.is_a?(Proc)
35
+
36
+ if class_logger.respond_to?(:fork)
37
+ if !@__logger.is_a?(Lumberjack::ForkedLogger) || @__logger&.parent_logger != class_logger
38
+ @__logger = class_logger.fork
39
+ end
40
+ else
41
+ @__logger = class_logger
42
+ end
43
+
44
+ @__logger
45
+ end
46
+
47
+ # Silence an individual event for this subscriber. The event name is the name of the public
48
+ # instance method to silence.
49
+ #
50
+ # @param event_name [String, Symbol] the name of the event to silence
51
+ # @return [void]
52
+ def silence_event!(event_name)
53
+ @__silenced_events ||= Set.new
54
+ @__silenced_events << event_name.to_s
55
+ end
56
+
57
+ # Unsilence an individual event for this subscriber.
58
+ #
59
+ # @param event_name [String, Symbol] the name of the event to unsilence
60
+ # @return [void]
61
+ def unsilence_event!(event_name)
62
+ @__silenced_events&.delete(event_name.to_s)
63
+ end
64
+
65
+ # Check if a specific event is silenced for this subscriber.
66
+ #
67
+ # @param event [String] the full event name of the event to check with the namespace
68
+ # @return [Boolean] true if the event is silenced, false otherwise
69
+ # @api private
70
+ def silenced_event?(event)
71
+ return false if @__silenced_events.nil?
72
+
73
+ i = event.index(".")
74
+ event_name = i ? event[0, i] : event
75
+ @__silenced_events.include?(event_name)
76
+ end
77
+ end
78
+
79
+ # Override the silenced? method to check if the event has been explicitly silenced.
80
+ #
81
+ # @param event [String] the event to check
82
+ # @return [Boolean] true if the event is silenced, false otherwise
83
+ def silenced?(event)
84
+ super || self.class.silenced_event?(event)
85
+ end
86
+
87
+ # Get the logger for this log subscriber instance.
88
+ #
89
+ # Delegates to the class-level logger method to ensure consistent
90
+ # behavior across all instances of the log subscriber.
91
+ #
92
+ # @return [Lumberjack::ForkedLogger, Logger] the logger instance for this subscriber
93
+ def logger
94
+ self.class.logger
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lumberjack::Rails
4
+ module RackLoggerExtension
5
+ private
6
+
7
+ def started_request_message(request)
8
+ return if Lumberjack::Rails.silence_rack_request_started?
9
+
10
+ super
11
+ end
12
+ end
13
+ end