activesupport-json_logging 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,57 @@
1
+ module JsonLogging
2
+ # Formatter proxy that delegates current_tags to the logger
3
+ # This is needed for ActiveJob which expects logger.formatter.current_tags
4
+ # Also implements call to ensure JSON formatting when formatter is used directly
5
+ class FormatterWithTags
6
+ def initialize(logger)
7
+ @logger = logger
8
+ end
9
+
10
+ def current_tags
11
+ @logger.send(:current_tags)
12
+ end
13
+
14
+ def call(severity, timestamp, progname, msg)
15
+ tags = current_tags
16
+ timestamp_str = Helpers.normalize_timestamp(timestamp)
17
+ payload = PayloadBuilder.build_base_payload(msg, severity: severity, timestamp: timestamp_str)
18
+ payload = PayloadBuilder.merge_context(payload, additional_context: JsonLogging.additional_context.compact, tags: tags)
19
+
20
+ "#{payload.compact.to_json}\n"
21
+ rescue => e
22
+ build_fallback_output(severity, timestamp, msg, e)
23
+ end
24
+
25
+ # Support tagged blocks for formatter
26
+ def tagged(*tags)
27
+ if block_given?
28
+ previous = @logger.send(:current_tags).dup
29
+ @logger.send(:push_tags, tags)
30
+ begin
31
+ yield @logger
32
+ ensure
33
+ @logger.send(:set_tags, previous)
34
+ end
35
+ else
36
+ @logger.send(:push_tags, tags)
37
+ self
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def build_fallback_output(severity, timestamp, msg, error)
44
+ timestamp_str = Helpers.normalize_timestamp(timestamp)
45
+ fallback_payload = {
46
+ timestamp: timestamp_str,
47
+ severity: severity,
48
+ message: Helpers.safe_string(msg),
49
+ formatter_error: {
50
+ class: Sanitizer.sanitize_string(error.class.name),
51
+ message: Sanitizer.sanitize_string(Helpers.safe_string(error.message))
52
+ }
53
+ }
54
+ "#{fallback_payload.compact.to_json}\n"
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,17 @@
1
+ module JsonLogging
2
+ module Helpers
3
+ module_function
4
+
5
+ # Normalize timestamp to ISO8601 with microseconds
6
+ def normalize_timestamp(timestamp)
7
+ (timestamp || Time.now).utc.iso8601(6)
8
+ end
9
+
10
+ # Safely convert object to string, never raises
11
+ def safe_string(obj)
12
+ obj.to_s
13
+ rescue
14
+ "<unprintable>"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ require "active_support"
2
+ require "active_support/logger"
3
+
4
+ module JsonLogging
5
+ class JsonLogger < ActiveSupport::Logger
6
+ # Include the extension module to get all JSON logging functionality
7
+ include JsonLoggerExtension
8
+
9
+ def initialize(*args, **kwargs)
10
+ # Initialize with minimal args to avoid ActiveSupport::Logger threading issues
11
+ # Rails 7+ supports kwargs, Rails 6 uses positional args only
12
+ logdev = args.first || $stdout
13
+ shift_age = args[1] || 0
14
+ shift_size = args[2]
15
+
16
+ # Handle both positional and keyword arguments for Rails 6-8 compatibility
17
+ if kwargs.empty? && shift_size
18
+ super(logdev, shift_age, shift_size)
19
+ elsif kwargs.empty?
20
+ super(logdev, shift_age)
21
+ else
22
+ # Rails 7+ may pass kwargs - delegate to parent
23
+ super
24
+ end
25
+
26
+ @formatter_with_tags = FormatterWithTags.new(self)
27
+ # Ensure parent class (Logger) also uses our formatter in case it uses it directly
28
+ instance_variable_set(:@formatter, @formatter_with_tags)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,177 @@
1
+ module JsonLogging
2
+ # Module that extends any Logger with JSON formatting capabilities
3
+ # This is used by JsonLogging.new to wrap standard loggers
4
+ module JsonLoggerExtension
5
+ SEVERITY_NAMES = {
6
+ ::Logger::DEBUG => "DEBUG",
7
+ ::Logger::INFO => "INFO",
8
+ ::Logger::WARN => "WARN",
9
+ ::Logger::ERROR => "ERROR",
10
+ ::Logger::FATAL => "FATAL",
11
+ ::Logger::UNKNOWN => "UNKNOWN"
12
+ }.freeze
13
+
14
+ def formatter
15
+ @formatter_with_tags ||= begin
16
+ formatter_with_tags = FormatterWithTags.new(self)
17
+ instance_variable_set(:@formatter, formatter_with_tags)
18
+ formatter_with_tags
19
+ end
20
+ end
21
+
22
+ def formatter=(formatter)
23
+ # Always use FormatterWithTags to ensure JSON formatting
24
+ formatter_with_tags = @formatter_with_tags || FormatterWithTags.new(self)
25
+ instance_variable_set(:@formatter, formatter_with_tags)
26
+ @formatter_with_tags = formatter_with_tags
27
+ end
28
+
29
+ def add(severity, message = nil, progname = nil)
30
+ return true if severity < level
31
+
32
+ msg = if message.nil?
33
+ if block_given?
34
+ yield
35
+ else
36
+ progname
37
+ end
38
+ else
39
+ message
40
+ end
41
+
42
+ payload = build_payload(severity, progname, msg)
43
+
44
+ stringified = stringify_keys(payload)
45
+ @logdev&.write("#{stringified.to_json}\n")
46
+ true
47
+ rescue => e
48
+ # Never fail logging - write a fallback entry
49
+ # Initialize msg if it wasn't set due to error
50
+ msg ||= "<uninitialized>"
51
+
52
+ fallback = {
53
+ timestamp: Helpers.normalize_timestamp(Time.now),
54
+ severity: severity_name(severity),
55
+ message: Sanitizer.sanitize_string(Helpers.safe_string(msg)),
56
+ logger_error: {
57
+ class: Sanitizer.sanitize_string(e.class.name),
58
+ message: Sanitizer.sanitize_string(Helpers.safe_string(e.message))
59
+ }
60
+ }
61
+ @logdev&.write("#{fallback.compact.to_json}\n")
62
+ true
63
+ end
64
+
65
+ # Override format_message to ensure it uses JSON formatting even if called directly
66
+ def format_message(severity, datetime, progname, msg)
67
+ formatter.call(severity, datetime, progname, msg)
68
+ end
69
+
70
+ # Native tag support compatible with Rails.logger.tagged
71
+ def tagged(*tags)
72
+ if block_given?
73
+ formatter.tagged(*tags) { yield self }
74
+ else
75
+ # Return a new wrapped logger with tags applied (similar to TaggedLogging)
76
+ logger = JsonLogging.new(self)
77
+ # Extend formatter with LocalTagStorage to preserve current tags when creating nested loggers
78
+ logger.formatter.extend(LocalTagStorage)
79
+ logger.formatter.push_tags(*formatter.current_tags, *tags)
80
+ logger
81
+ end
82
+ end
83
+
84
+ # Flush tags (used by Rails when request completes)
85
+ def flush
86
+ clear_tags!
87
+ super if defined?(super)
88
+ end
89
+
90
+ private
91
+
92
+ def tags_key
93
+ @tags_key ||= :"json_logging_tags_#{object_id}"
94
+ end
95
+
96
+ def current_tags
97
+ # Use IsolatedExecutionState (Rails 7.1+) for better thread/Fiber safety
98
+ # Falls back to Thread.current for Rails 6-7.0
99
+ if defined?(ActiveSupport::IsolatedExecutionState)
100
+ ActiveSupport::IsolatedExecutionState[tags_key] ||= []
101
+ else
102
+ Thread.current[tags_key] ||= []
103
+ end
104
+ end
105
+
106
+ def set_tags(new_tags)
107
+ # Use IsolatedExecutionState (Rails 7.1+) for better thread/Fiber safety
108
+ # Falls back to Thread.current for Rails 6-7.0
109
+ if defined?(ActiveSupport::IsolatedExecutionState)
110
+ ActiveSupport::IsolatedExecutionState[tags_key] = new_tags
111
+ else
112
+ Thread.current[tags_key] = new_tags
113
+ end
114
+ end
115
+
116
+ def push_tags(tags)
117
+ flat = tags.flatten.compact.map(&:to_s).reject(&:empty?)
118
+ return if flat.empty?
119
+ set_tags(current_tags + flat)
120
+ end
121
+
122
+ def clear_tags!
123
+ set_tags([])
124
+ end
125
+
126
+ def build_payload(severity, _progname, msg)
127
+ payload = PayloadBuilder.build_base_payload(
128
+ msg,
129
+ severity: severity_name(severity),
130
+ timestamp: Helpers.normalize_timestamp(Time.now)
131
+ )
132
+ payload = PayloadBuilder.merge_context(
133
+ payload,
134
+ additional_context: JsonLogging.additional_context.compact,
135
+ tags: current_tags
136
+ )
137
+
138
+ payload.compact
139
+ end
140
+
141
+ def severity_name(severity)
142
+ SEVERITY_NAMES[severity] || severity.to_s
143
+ end
144
+
145
+ def stringify_keys(hash)
146
+ case hash
147
+ when Hash
148
+ hash.each_with_object({}) do |(k, v), result|
149
+ result[k.to_s] = stringify_keys(v)
150
+ end
151
+ when Array
152
+ hash.map { |v| stringify_keys(v) }
153
+ else
154
+ hash
155
+ end
156
+ end
157
+ end
158
+
159
+ # Module for preserving current tags when creating nested tagged loggers
160
+ # Similar to ActiveSupport::TaggedLogging::LocalTagStorage
161
+ # When extended on a formatter, stores tags locally instead of using thread-local storage
162
+ module LocalTagStorage
163
+ def self.extended(base)
164
+ base.instance_variable_set(:@local_tags, [])
165
+ end
166
+
167
+ def push_tags(*tags)
168
+ flat = tags.flatten.compact.map(&:to_s).reject(&:empty?)
169
+ return if flat.empty?
170
+ @local_tags = (@local_tags || []) + flat
171
+ end
172
+
173
+ def current_tags
174
+ @local_tags || []
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,32 @@
1
+ module JsonLogging
2
+ module MessageParser
3
+ module_function
4
+
5
+ def parse_message(msg)
6
+ if msg.is_a?(Hash) || (msg.respond_to?(:to_hash) && !msg.is_a?(String))
7
+ # Sanitize hash messages
8
+ Sanitizer.sanitize_hash(msg.to_hash)
9
+ elsif msg.is_a?(String) && json_string?(msg)
10
+ begin
11
+ parsed = JSON.parse(msg)
12
+ # Sanitize parsed JSON structure
13
+ Sanitizer.sanitize_value(parsed)
14
+ rescue JSON::ParserError
15
+ # If JSON parsing fails, sanitize the raw string
16
+ Sanitizer.sanitize_string(msg)
17
+ end
18
+ elsif msg.is_a?(Exception)
19
+ # Handle exceptions specially with sanitization
20
+ Sanitizer.sanitize_exception(msg)
21
+ else
22
+ # Sanitize other types
23
+ Sanitizer.sanitize_value(msg)
24
+ end
25
+ end
26
+
27
+ def json_string?(str)
28
+ (str.start_with?("{") && str.end_with?("}")) ||
29
+ (str.start_with?("[") && str.end_with?("]"))
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,46 @@
1
+ module JsonLogging
2
+ module PayloadBuilder
3
+ module_function
4
+
5
+ def build_base_payload(msg, severity: nil, timestamp: nil)
6
+ parsed = MessageParser.parse_message(msg)
7
+ payload = {}
8
+
9
+ if parsed.is_a?(Hash)
10
+ payload.merge!(parsed)
11
+ else
12
+ payload[:message] = parsed
13
+ end
14
+
15
+ payload[:severity] = severity if severity
16
+ payload[:timestamp] = timestamp if timestamp
17
+
18
+ payload
19
+ end
20
+
21
+ def merge_context(payload, additional_context:, tags: [])
22
+ existing_context = payload[:context].is_a?(Hash) ? payload[:context] : {}
23
+
24
+ # Sanitize additional context before merging (filters sensitive keys, limits size, sanitizes strings only)
25
+ # Only sanitize if it's a hash - preserve other types
26
+ sanitized_context = if additional_context.is_a?(Hash) && !additional_context.empty?
27
+ Sanitizer.sanitize_hash(additional_context)
28
+ else
29
+ additional_context || {}
30
+ end
31
+
32
+ deduped_additional = sanitized_context.reject { |k, _| payload.key?(k) }
33
+ merged_context = existing_context.merge(deduped_additional)
34
+
35
+ unless tags.empty?
36
+ existing_tags = Array(merged_context[:tags])
37
+ # Sanitize tag strings (remove control chars, truncate)
38
+ sanitized_tags = tags.map { |tag| Sanitizer.sanitize_string(tag.to_s) }
39
+ merged_context[:tags] = (existing_tags + sanitized_tags).uniq
40
+ end
41
+
42
+ payload[:context] = merged_context unless merged_context.empty?
43
+ payload
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,158 @@
1
+ module JsonLogging
2
+ module Sanitizer
3
+ # Control characters that should be escaped or removed from log messages
4
+ CONTROL_CHARS = /[\x00-\x1F\x7F]/
5
+
6
+ # Maximum string length before truncation
7
+ MAX_STRING_LENGTH = 10_000
8
+
9
+ # Maximum context hash size (number of keys)
10
+ MAX_CONTEXT_SIZE = 50
11
+
12
+ # Maximum depth for nested structures
13
+ MAX_DEPTH = 10
14
+
15
+ # Maximum backtrace lines to include
16
+ MAX_BACKTRACE_LINES = 20
17
+
18
+ # Common sensitive key patterns (case insensitive) - fallback when Rails ParameterFilter not available
19
+ SENSITIVE_KEY_PATTERNS = /\b(password|passwd|pwd|secret|token|api_key|apikey|access_token|auth_token|private_key|credential)\b/i
20
+
21
+ module_function
22
+
23
+ # Get Rails ParameterFilter if available, nil otherwise
24
+ def rails_parameter_filter
25
+ return nil unless defined?(Rails) && Rails.respond_to?(:application)
26
+ return nil unless Rails.application.respond_to?(:config)
27
+
28
+ filter_params = Rails.application.config.filter_parameters
29
+ return nil if filter_params.empty?
30
+
31
+ ActiveSupport::ParameterFilter.new(filter_params)
32
+ rescue
33
+ nil
34
+ end
35
+
36
+ # Sanitize a string by removing/escaping control characters and truncating
37
+ def sanitize_string(str)
38
+ return str unless str.is_a?(String)
39
+
40
+ # Remove or replace control characters
41
+ sanitized = str.gsub(CONTROL_CHARS, "")
42
+
43
+ # Truncate if too long
44
+ if sanitized.length > MAX_STRING_LENGTH
45
+ sanitized = sanitized[0, MAX_STRING_LENGTH] + "...[truncated]"
46
+ end
47
+
48
+ sanitized
49
+ rescue
50
+ "<sanitization_error>"
51
+ end
52
+
53
+ # Sanitize a hash, removing sensitive keys and limiting size/depth
54
+ # Uses Rails ParameterFilter when available, falls back to pattern matching
55
+ def sanitize_hash(hash, depth: 0)
56
+ return hash unless hash.is_a?(Hash)
57
+
58
+ # Prevent excessive nesting
59
+ return {"error" => "max_depth_exceeded"} if depth > MAX_DEPTH
60
+
61
+ # Limit hash size first
62
+ limited_hash = if hash.size > MAX_CONTEXT_SIZE
63
+ truncated = hash.first(MAX_CONTEXT_SIZE).to_h
64
+ truncated["_truncated"] = true
65
+ truncated
66
+ else
67
+ hash
68
+ end
69
+
70
+ # Use Rails ParameterFilter if available (handles encrypted attributes automatically)
71
+ filter = rails_parameter_filter
72
+ if filter
73
+ # ParameterFilter will filter based on Rails.config.filter_parameters
74
+ # This includes encrypted attributes automatically
75
+ # Create a deep copy since filter modifies in place (Rails 6+)
76
+ filtered = limited_hash.respond_to?(:deep_dup) ? limited_hash.deep_dup : limited_hash.dup
77
+ filtered = filter.filter(filtered)
78
+
79
+ # Then sanitize values (strings, control chars, etc.) preserving filtered structure
80
+ filtered.each_with_object({}) do |(key, value), result|
81
+ result[key] = sanitize_value(value, depth: depth + 1)
82
+ end
83
+
84
+ else
85
+ # Fallback: use pattern matching for sensitive keys
86
+ limited_hash.each_with_object({}) do |(key, value), result|
87
+ key_str = key.to_s
88
+
89
+ # Skip sensitive keys
90
+ if SENSITIVE_KEY_PATTERNS.match?(key_str)
91
+ result[key_str.gsub(/(?<!^)(?=[A-Z])/, "_").downcase + "_filtered"] = "[FILTERED]"
92
+ next
93
+ end
94
+
95
+ result[key] = sanitize_value(value, depth: depth + 1)
96
+ end
97
+ end
98
+ rescue
99
+ {"sanitization_error" => true}
100
+ end
101
+
102
+ # Sanitize a value (handles strings, hashes, arrays, etc.)
103
+ # Preserves numeric, boolean, and nil types
104
+ def sanitize_value(value, depth: 0)
105
+ case value
106
+ when String
107
+ sanitize_string(value)
108
+ when Hash
109
+ sanitize_hash(value, depth: depth)
110
+ when Array
111
+ # Limit array size
112
+ sanitized = value.first(MAX_CONTEXT_SIZE).map { |v| sanitize_value(v, depth: depth + 1) }
113
+ sanitized << "[truncated]" if value.size > MAX_CONTEXT_SIZE
114
+ sanitized
115
+ when Exception
116
+ sanitize_exception(value)
117
+ when Numeric, TrueClass, FalseClass, NilClass
118
+ # Preserve numeric, boolean, and nil types
119
+ value
120
+ else
121
+ # For other types, convert to string and sanitize
122
+ sanitize_string(value.to_s)
123
+ end
124
+ rescue
125
+ "<unprintable>"
126
+ end
127
+
128
+ # Sanitize exception, including backtrace
129
+ def sanitize_exception(ex)
130
+ {
131
+ "error" => {
132
+ "class" => ex.class.name,
133
+ "message" => sanitize_string(ex.message.to_s),
134
+ "backtrace" => sanitize_backtrace(ex.backtrace)
135
+ }
136
+ }
137
+ rescue
138
+ {"error" => {"class" => "Exception", "message" => "<sanitization_failed>"}}
139
+ end
140
+
141
+ # Sanitize backtrace - truncate and remove sensitive paths
142
+ def sanitize_backtrace(backtrace)
143
+ return [] unless backtrace.is_a?(Array)
144
+
145
+ # Take first MAX_BACKTRACE_LINES, sanitize each
146
+ backtrace.first(MAX_BACKTRACE_LINES).map do |line|
147
+ sanitize_string(line.to_s)
148
+ end
149
+ rescue
150
+ []
151
+ end
152
+
153
+ # Check if a key looks sensitive
154
+ def sensitive_key?(key)
155
+ SENSITIVE_KEY_PATTERNS.match?(key.to_s)
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,3 @@
1
+ module JsonLogging
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,124 @@
1
+ require "logger"
2
+ require "json"
3
+ require "time"
4
+
5
+ require_relative "json_logging/version"
6
+ require_relative "json_logging/helpers"
7
+ require_relative "json_logging/sanitizer"
8
+ require_relative "json_logging/message_parser"
9
+ require_relative "json_logging/payload_builder"
10
+ require_relative "json_logging/formatter"
11
+ require_relative "json_logging/formatter_with_tags"
12
+ require_relative "json_logging/json_logger_extension"
13
+ require_relative "json_logging/json_logger"
14
+
15
+ module JsonLogging
16
+ THREAD_CONTEXT_KEY = :__json_logging_context
17
+
18
+ def self.with_context(extra_context)
19
+ original = Thread.current[THREAD_CONTEXT_KEY]
20
+ Thread.current[THREAD_CONTEXT_KEY] = (original || {}).merge(safe_hash(extra_context))
21
+ yield
22
+ ensure
23
+ Thread.current[THREAD_CONTEXT_KEY] = original
24
+ end
25
+
26
+ # Returns the current thread-local context when called without arguments,
27
+ # or sets a transformer when called with a block or assigned a proc.
28
+ #
29
+ # @example Getting context
30
+ # JsonLogging.additional_context # => {user_id: 123, ...}
31
+ #
32
+ # @example Setting transformer with block
33
+ # JsonLogging.additional_context do |context|
34
+ # context.merge(environment: Rails.env, hostname: Socket.gethostname)
35
+ # end
36
+ #
37
+ # @example Setting transformer with assignment
38
+ # JsonLogging.additional_context = ->(context) { context.merge(env: Rails.env) }
39
+ def self.additional_context(*args, &block)
40
+ if args.any? || block_given?
41
+ return public_send(:additional_context=, args.first || block)
42
+ end
43
+
44
+ begin
45
+ base_context = (Thread.current[THREAD_CONTEXT_KEY] || {}).dup
46
+ rescue
47
+ base_context = {}
48
+ end
49
+
50
+ transformer = @additional_context_transformer
51
+ if transformer.is_a?(Proc)
52
+ begin
53
+ transformer.call(base_context)
54
+ rescue
55
+ base_context
56
+ end
57
+ else
58
+ base_context
59
+ end
60
+ end
61
+
62
+ def self.additional_context=(proc_or_block)
63
+ @additional_context_transformer = proc_or_block
64
+ end
65
+
66
+ def self.safe_hash(obj)
67
+ obj.is_a?(Hash) ? obj : {}
68
+ rescue
69
+ {}
70
+ end
71
+
72
+ # Returns an `ActiveSupport::Logger` that has already been wrapped with JSON logging concern.
73
+ #
74
+ # @param *args Arguments passed to ActiveSupport::Logger.new
75
+ # @param **kwargs Keyword arguments passed to ActiveSupport::Logger.new
76
+ # @return [Logger] A logger wrapped with JSON formatting and tagged logging support
77
+ #
78
+ # @example
79
+ # logger = JsonLogging.logger($stdout)
80
+ # logger.info("Stuff") # Logs JSON formatted entry
81
+ def self.logger(*args, **kwargs)
82
+ new(ActiveSupport::Logger.new(*args, **kwargs))
83
+ end
84
+
85
+ # Wraps any standard Logger object to provide JSON formatting capabilities.
86
+ # Similar to ActiveSupport::TaggedLogging.new
87
+ #
88
+ # @param logger [Logger] Any standard Logger object (Logger, ActiveSupport::Logger, etc.)
89
+ # @return [Logger] A logger extended with JSON formatting and tagged logging support
90
+ #
91
+ # @example
92
+ # logger = JsonLogging.new(Logger.new(STDOUT))
93
+ # logger.info("Stuff") # Logs JSON formatted entry
94
+ #
95
+ # @example With tagged logging
96
+ # logger = JsonLogging.new(Logger.new(STDOUT))
97
+ # logger.tagged("BCX") { logger.info("Stuff") } # Logs with tags
98
+ # logger.tagged("BCX").info("Stuff") # Logs with tags (non-block form)
99
+ def self.new(logger)
100
+ logger = logger.clone
101
+ if logger.formatter
102
+ logger.formatter = logger.formatter.clone
103
+
104
+ # Workaround for https://bugs.ruby-lang.org/issues/20250
105
+ # Can be removed when Ruby 3.4 is the least supported version.
106
+ logger.formatter.object_id if logger.formatter.is_a?(Proc)
107
+ else
108
+ # Ensure we set a default formatter so we aren't extending nil!
109
+ # Use ActiveSupport::Logger::SimpleFormatter if available, otherwise default Logger::Formatter
110
+ logger.formatter = if defined?(ActiveSupport::Logger::SimpleFormatter)
111
+ ActiveSupport::Logger::SimpleFormatter.new
112
+ else
113
+ ::Logger::Formatter.new
114
+ end
115
+ end
116
+
117
+ logger.extend(JsonLoggerExtension)
118
+ formatter_with_tags = FormatterWithTags.new(logger)
119
+ logger.instance_variable_set(:@formatter_with_tags, formatter_with_tags)
120
+ logger.formatter = formatter_with_tags
121
+
122
+ logger
123
+ end
124
+ end