unified_logger 0.1.3

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8852d1fcbbbc53507a9d23e79b9e08405955b89c60458cd91e7d3305e71a7be4
4
+ data.tar.gz: 46a8534b601d0206ebffdb7237066835d436e960bf0cf17c7f5831d710b00652
5
+ SHA512:
6
+ metadata.gz: 7669986b85d090ae4098b62dd28a9a0b17bf08568b0ae8d75c1a857c3d2ca6060937128a3f6dd31f17216f7e5120f617eecb5c242e14548350a61be46d77bcb4
7
+ data.tar.gz: 81dfd77bfe8205b311ad2dac30fd3f4f7dfcf4aaf6e0a6ab3f5bff84e754cbfbe1ea07b8d925c33a6dfee4147fee6a204eac9d6250dba5c2fe63cda7b7ccb817
@@ -0,0 +1,46 @@
1
+ require "English"
2
+
3
+ module UnifiedLogger
4
+ class JobLogger
5
+ DEFAULT_MAX_RETRIES = 5
6
+
7
+ class << self
8
+ def log(job)
9
+ yield
10
+ ensure
11
+ log_execution(job) if UnifiedLogger.current_logger.is_a?(UnifiedLogger::Logger)
12
+ end
13
+
14
+ private
15
+
16
+ def log_execution(job)
17
+ log = {
18
+ log_type: :job,
19
+ timestamp: UnifiedLogger.formatted_time,
20
+ class_name: job.class.name,
21
+ id: job.job_id,
22
+ queue: job.queue_name,
23
+ params: job.arguments,
24
+ executions_count: job.executions,
25
+ exception_executions: job.exception_executions,
26
+ enqueued_at: job.enqueued_at,
27
+ locale: job.locale,
28
+ duration: job.enqueued_at.present? ? UnifiedLogger.current_time - job.enqueued_at.in_time_zone : "unknown"
29
+ }
30
+ log[:custom] = UnifiedLogger::Logger.fetch_and_reset_custom_logs if UnifiedLogger::Logger.custom_logs.any?
31
+
32
+ if $ERROR_INFO
33
+ log[:exception] = UnifiedLogger::Logger.format_exception($ERROR_INFO)
34
+ log[:status] = job.executions >= DEFAULT_MAX_RETRIES ? :error : :warn
35
+ else
36
+ log[:status] = :ok
37
+ end
38
+
39
+ custom = {}
40
+ UnifiedLogger.transform_job_log_callable&.call(custom)
41
+ log.merge!(custom)
42
+ UnifiedLogger.current_logger.write(UnifiedLogger::Logger.format(log))
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,157 @@
1
+ module UnifiedLogger
2
+ class Logger < ::Logger
3
+ CUSTOM_LOGS = Concurrent::ThreadLocalVar.new([])
4
+ SEVERITY_LEVELS = {
5
+ debug: ::Logger::DEBUG,
6
+ info: ::Logger::INFO,
7
+ warn: ::Logger::WARN,
8
+ error: ::Logger::ERROR,
9
+ fatal: ::Logger::FATAL,
10
+ unknown: ::Logger::UNKNOWN
11
+ }.freeze
12
+ SEVERITY_MAP = SEVERITY_LEVELS.invert.freeze
13
+
14
+ def initialize(logging_device = $stdout, *)
15
+ super
16
+ @logging_device = logging_device
17
+ self.formatter = proc {}
18
+ end
19
+
20
+ def debug(message = nil, &block)
21
+ message = block.call if message.nil? && block
22
+ add(::Logger::DEBUG, message)
23
+ end
24
+
25
+ def info(message = nil, &block)
26
+ message = block.call if message.nil? && block
27
+ add(::Logger::INFO, message)
28
+ end
29
+
30
+ def warn(message = nil, &block)
31
+ message = block.call if message.nil? && block
32
+ add(::Logger::WARN, message)
33
+ end
34
+
35
+ def error(message = nil, &block)
36
+ message = block.call if message.nil? && block
37
+ add(::Logger::ERROR, message)
38
+ end
39
+
40
+ def fatal(message = nil, &block)
41
+ message = block.call if message.nil? && block
42
+ add(::Logger::FATAL, message)
43
+ end
44
+
45
+ def unknown(message = nil, &block)
46
+ message = block.call if message.nil? && block
47
+ add(::Logger::UNKNOWN, message)
48
+ end
49
+
50
+ def <<(message)
51
+ add(::Logger::UNKNOWN, message.to_s.chomp)
52
+ self
53
+ end
54
+
55
+ def write(message)
56
+ @logging_device.write("#{message}\n") if @logging_device.respond_to?(:write)
57
+ end
58
+
59
+ class << self
60
+ def custom_logs
61
+ CUSTOM_LOGS.value
62
+ end
63
+
64
+ def reset_thread_logs
65
+ CUSTOM_LOGS.value = []
66
+ end
67
+
68
+ def fetch_and_reset_custom_logs
69
+ logs = custom_logs
70
+ reset_thread_logs
71
+ logs
72
+ end
73
+
74
+ def trim(data)
75
+ data = filter(data)
76
+ size = data.inspect.length
77
+ max = UnifiedLogger.config[:max_log_field_size]
78
+ return data if size < max
79
+
80
+ begin
81
+ json = JSON.generate(data, quirks_mode: true)
82
+ rescue JSON::GeneratorError, Encoding::UndefinedConversionError
83
+ json = data.respond_to?("to_s") ? data.to_s : "unparseable object (instance of #{data.class.name})"
84
+ end
85
+ "#{json[0..max]}... (#{size - max} extra characters omitted)"
86
+ end
87
+
88
+ def format_exception(exception)
89
+ if exception.is_a?(String)
90
+ { message: exception }
91
+ elsif exception.is_a?(Exception)
92
+ btc = ActiveSupport::BacktraceCleaner.new
93
+ prefix = UnifiedLogger.backtrace_root + File::SEPARATOR
94
+ btc.add_filter { |line| line.sub(/\A#{Regexp.escape(prefix)}/, "") }
95
+ btc.add_silencer { |line| /request_logger.rb/.match?(line) }
96
+ { class_name: exception.class.name, message: exception.message, backtrace: btc.clean(exception.backtrace || []) }
97
+ elsif exception.respond_to?(:to_s)
98
+ exception.to_s
99
+ else
100
+ exception.class.name
101
+ end
102
+ end
103
+
104
+ def format(log)
105
+ filtered_log = filter(log)
106
+ formatter = UnifiedLogger.log_transformer
107
+ formatter.present? ? formatter.call(filtered_log) : filtered_log.to_json
108
+ end
109
+
110
+ private
111
+
112
+ def filter(content)
113
+ return content unless content.respond_to?(:each)
114
+
115
+ filter_class = if defined?(ActiveSupport::ParameterFilter)
116
+ ActiveSupport::ParameterFilter
117
+ elsif defined?(ActionDispatch::Http::ParameterFilter)
118
+ ActionDispatch::Http::ParameterFilter
119
+ end
120
+ return content unless filter_class
121
+
122
+ filter_class.new(UnifiedLogger.config[:filter_params]).filter(content)
123
+ end
124
+ end
125
+
126
+ def add(severity, message = nil, progname = nil, &block)
127
+ if message.nil?
128
+ message = block ? block.call : progname
129
+ end
130
+
131
+ return true if message.blank?
132
+ return true unless severity >= level
133
+
134
+ severity_symbol = SEVERITY_MAP[severity] || :unknown
135
+ append_custom_log(severity_symbol, message)
136
+ end
137
+
138
+ private
139
+
140
+ def append_custom_log(severity, message)
141
+ message = sanitize_log_message(message) if message.is_a?(String)
142
+ log_hash = { timestamp: UnifiedLogger.formatted_time, severity: severity, message: message }
143
+
144
+ CUSTOM_LOGS.value = CUSTOM_LOGS.value + [log_hash]
145
+ end
146
+
147
+ def sanitize_log_message(text)
148
+ return text unless text.is_a?(String)
149
+
150
+ text = text.gsub(/\e\[[0-9;]*m/, "")
151
+ text = text.gsub(%r{[^a-zA-Z0-9\s.,;:!?'"()\[\]{}\-_@#$%&*+=<>|~`/]}, "")
152
+ text = text.gsub('"', "'")
153
+ text = text.gsub(/\s+/, " ")
154
+ text.strip
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UnifiedLogger
4
+ class Railtie < Rails::Railtie
5
+ initializer "unified_logger.middleware", after: :load_config_initializers do |app|
6
+ if UnifiedLogger.config[:auto_insert_middleware]
7
+ app.middleware.insert_after ActionDispatch::DebugExceptions,
8
+ UnifiedLogger::RequestLogger
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,109 @@
1
+ require "English"
2
+
3
+ module UnifiedLogger
4
+ class RequestLogger
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ return @app.call(env) unless UnifiedLogger.current_logger.is_a?(UnifiedLogger::Logger)
11
+
12
+ started = UnifiedLogger.current_time
13
+ status, headers, response = @app.call(env)
14
+ ensure
15
+ if UnifiedLogger.current_logger.is_a?(UnifiedLogger::Logger) && !silenced?(env["REQUEST_PATH"])
16
+ log = build_log(started, env, status, headers, response)
17
+ custom = {}
18
+ UnifiedLogger.transform_request_log_callable&.call(custom, env)
19
+ log.merge!(custom)
20
+ UnifiedLogger.current_logger.write(UnifiedLogger::Logger.format(log))
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def silenced?(path)
27
+ UnifiedLogger.config[:silence_paths].any? do |pattern|
28
+ pattern.is_a?(Regexp) ? pattern.match?(path) : pattern == path
29
+ end
30
+ end
31
+
32
+ def build_log(started, env, status = nil, headers = nil, response = nil)
33
+ status ||= 500
34
+ headers ||= {}
35
+ response ||= ""
36
+ path_parameters = env["action_dispatch.request.path_parameters"] || {}
37
+ query_string = env["QUERY_STRING"]
38
+ log = {
39
+ log_type: :request,
40
+ timestamp: UnifiedLogger.formatted_time,
41
+ id: env["action_dispatch.request_id"],
42
+ ip: env["action_dispatch.remote_ip"].to_s,
43
+ controller: path_parameters[:controller],
44
+ action: path_parameters[:action],
45
+ request: {
46
+ path: env["REQUEST_PATH"],
47
+ method: env["REQUEST_METHOD"],
48
+ headers: env.select { |e| e.start_with?("HTTP_") }.reject { |h| h.start_with?("HTTP_SEC_") },
49
+ path_params: path_parameters.except(:controller, :action),
50
+ query_params: query_string.blank? ? {} : Rack::Utils.parse_nested_query(query_string),
51
+ body: request_body(env)
52
+ },
53
+ response: {
54
+ headers: headers,
55
+ status: status,
56
+ body: response_body(response, headers["content-type"])
57
+ },
58
+ thread_id: Thread.current.object_id,
59
+ process_id: Process.pid,
60
+ duration: started ? UnifiedLogger.current_time - started : 0
61
+ }
62
+ log[:exception] = UnifiedLogger::Logger.format_exception($ERROR_INFO) if $ERROR_INFO.present?
63
+ log[:custom] = UnifiedLogger::Logger.fetch_and_reset_custom_logs if UnifiedLogger::Logger.custom_logs.any?
64
+
65
+ log
66
+ end
67
+
68
+ def request_body(env)
69
+ return nil unless (input = env["rack.input"])
70
+ return env["rack.request.form_hash"] if env["rack.request.form_hash"]
71
+
72
+ input.rewind
73
+ body = input.read
74
+ input.rewind
75
+
76
+ parse_body(body, env["CONTENT_TYPE"])
77
+ end
78
+
79
+ def response_body(response, content_type)
80
+ return nil if response.nil? || (response.respond_to?(:empty?) && response.empty?)
81
+ return nil if content_type&.exclude?("application/") && content_type.exclude?("text/plain")
82
+ return response_body(response.body, content_type) if response.respond_to?(:body)
83
+
84
+ body = response.respond_to?(:map) ? response.join : response.to_s
85
+
86
+ parse_body(body, content_type)
87
+ ensure
88
+ response.close if response.respond_to?(:close)
89
+ end
90
+
91
+ def parse_body(body, content_type)
92
+ return nil if body.nil?
93
+ return nil if body.empty?
94
+
95
+ main_content_type = content_type&.split(";")&.first&.strip&.downcase
96
+
97
+ case main_content_type
98
+ when "application/json"
99
+ JSON.parse(body)
100
+ when "application/x-www-form-urlencoded"
101
+ Rack::Utils.parse_nested_query(body)
102
+ else
103
+ UnifiedLogger::Logger.trim(body)
104
+ end
105
+ rescue JSON::ParserError, TypeError, ArgumentError
106
+ body
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,3 @@
1
+ module UnifiedLogger
2
+ VERSION = "0.1.3".freeze
3
+ end
@@ -0,0 +1,88 @@
1
+ require "concurrent"
2
+ require "active_support/core_ext/object/blank"
3
+ require "active_support/core_ext/enumerable"
4
+ begin
5
+ require "active_support/parameter_filter"
6
+ rescue LoadError
7
+ # ActiveSupport < 6.0; falls back to ActionDispatch::Http::ParameterFilter at runtime
8
+ end
9
+ require "active_support/backtrace_cleaner"
10
+ require "json"
11
+ require "logger"
12
+
13
+ require_relative "unified_logger/version"
14
+ require_relative "unified_logger/logger"
15
+ require_relative "unified_logger/request_logger"
16
+ require_relative "unified_logger/job_logger"
17
+
18
+ module UnifiedLogger
19
+ class DoubleDefineError < StandardError; end
20
+
21
+ DEFAULTS = {
22
+ max_log_field_size: 2048,
23
+ filter_params: %i[passw secret token crypt salt certificate otp ssn set-cookie http_authorization http_cookie pin],
24
+ auto_insert_middleware: true,
25
+ silence_paths: []
26
+ }.freeze
27
+
28
+ def self.config
29
+ @config ||= DEFAULTS.dup
30
+ end
31
+
32
+ def self.configure(options = {})
33
+ config.merge!(options)
34
+ end
35
+
36
+ class << self
37
+ attr_reader :transform_request_log_callable, :transform_job_log_callable, :log_transformer
38
+
39
+ def transform_request_log=(callable)
40
+ raise DoubleDefineError, "transform_request_log already defined" if @transform_request_log_callable
41
+
42
+ @transform_request_log_callable = callable
43
+ end
44
+
45
+ def transform_job_log=(callable)
46
+ raise DoubleDefineError, "transform_job_log already defined" if @transform_job_log_callable
47
+
48
+ @transform_job_log_callable = callable
49
+ end
50
+
51
+ def log_transformer=(callable)
52
+ raise DoubleDefineError, "log_transformer already defined" if @log_transformer
53
+
54
+ @log_transformer = callable
55
+ end
56
+
57
+ delegate :trim, :format, :format_exception,
58
+ :custom_logs, :fetch_and_reset_custom_logs, :reset_thread_logs,
59
+ to: :"UnifiedLogger::Logger"
60
+ end
61
+
62
+ def self.backtrace_root
63
+ if defined?(Rails)
64
+ Rails.root.to_s
65
+ else
66
+ Dir.pwd
67
+ end
68
+ end
69
+
70
+ def self.current_logger
71
+ return unless defined?(Rails)
72
+
73
+ return Rails.logger if Rails.logger.is_a?(UnifiedLogger::Logger)
74
+ return Rails.logger unless Rails.logger.respond_to?(:broadcasts)
75
+
76
+ Rails.logger.broadcasts.find { |l| l.is_a?(UnifiedLogger::Logger) } || Rails.logger
77
+ end
78
+
79
+ def self.current_time
80
+ Time.zone&.now || Time.now.utc
81
+ end
82
+
83
+ def self.formatted_time
84
+ current_time.iso8601(3)
85
+ end
86
+ end
87
+
88
+ require "unified_logger/railtie" if defined?(Rails::Railtie)
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: unified_logger
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Marcovecchio
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-03-23 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '4.2'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '4.2'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9'
32
+ - !ruby/object:Gem::Dependency
33
+ name: concurrent-ruby
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '1.0'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '1.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rack
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '1.6'
53
+ - - "<"
54
+ - !ruby/object:Gem::Version
55
+ version: '4'
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '1.6'
63
+ - - "<"
64
+ - !ruby/object:Gem::Version
65
+ version: '4'
66
+ description: One JSON log line per request or job. Captures request/response data,
67
+ in-app logger calls, exceptions with cleaned backtraces, and sensitive data filtering
68
+ — all in a single event.
69
+ executables: []
70
+ extensions: []
71
+ extra_rdoc_files: []
72
+ files:
73
+ - lib/unified_logger.rb
74
+ - lib/unified_logger/job_logger.rb
75
+ - lib/unified_logger/logger.rb
76
+ - lib/unified_logger/railtie.rb
77
+ - lib/unified_logger/request_logger.rb
78
+ - lib/unified_logger/version.rb
79
+ homepage: https://github.com/marcovecchio/unified_logger
80
+ licenses:
81
+ - MIT
82
+ metadata:
83
+ rubygems_mfa_required: 'true'
84
+ source_code_uri: https://github.com/marcovecchio/unified_logger
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '2.4'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.6.7
100
+ specification_version: 4
101
+ summary: Structured JSON logging for Rack and Rails applications
102
+ test_files: []