unified_logger 0.1.5 → 0.1.7

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: a0ea9e1937937ec836c644c3831302f34b27547cb06bb76a3ef4ccb089e98a90
4
- data.tar.gz: d5534540295b6ee5521fe095322844e64ff5f3c9da528dec743e6919540f956e
3
+ metadata.gz: ff3a0a9f560baae4bcae1a17b3b4d8ef7aebd8777649f544c5fc818f4d24afa7
4
+ data.tar.gz: b2a9e96b83a7cf67502acc2af0d9008ba39fdef01109af2e5462e5a56a79477d
5
5
  SHA512:
6
- metadata.gz: e38c0618abb36baf5dfc6fbd308925f78e5dbabf9a5913072bf2fa116936b7ae9519406966db32aaa373efe97ac9a90d7210881a10d378f4efa1cc089b3e7fc0
7
- data.tar.gz: cbf6d324fa0ce67b90313427c07234b1ec16864f365fbd2c287a5ecbabd4de993ba50a3d8d655e520f96d876c462aed2e2116f9cec9f12c8687334e3993fa36f
6
+ metadata.gz: b798053594157ef35351db35ea2937867bdb69d41a1280ec08a6e577c9d729634f45352781f3baeff81274dcb46399bcdb883ad0b181bbf2ce7cc85b83707721
7
+ data.tar.gz: 97ad2f940f4889b7665cac57fc2318b91387bebc3d5400c2d4afb4b20f5d8efcd7b008b29e8286eb14992e7bbd0e896351ec2775e3d13cecc47ebd688956160c
@@ -1,41 +1,61 @@
1
1
  module UnifiedLogger
2
2
  class JobLogger
3
- DEFAULT_MAX_RETRIES = 5
4
-
5
3
  class << self
6
- def log(job)
4
+ def log(class_name:, id: nil, queue: nil, params: nil,
5
+ enqueued_at: nil, retry_count: 0, max_retries: nil, **extra)
6
+ started = UnifiedLogger.current_time
7
7
  yield
8
8
  ensure
9
- log_execution(job) if UnifiedLogger.current_logger.is_a?(UnifiedLogger::Logger)
9
+ if UnifiedLogger.current_logger.is_a?(UnifiedLogger::Logger)
10
+ write_log(class_name: class_name, id: id, queue: queue, params: params,
11
+ enqueued_at: enqueued_at, retry_count: retry_count,
12
+ max_retries: max_retries, started: started, **extra)
13
+ end
10
14
  end
11
15
 
12
16
  private
13
17
 
14
- def log_execution(job)
18
+ def write_log(class_name:, id:, queue:, params:, enqueued_at:,
19
+ retry_count:, max_retries:, started:, **extra)
20
+ enqueued_time = parse_time(enqueued_at)
21
+
15
22
  log = {
16
- log_type: :job,
17
- timestamp: UnifiedLogger.formatted_time,
18
- class_name: job.class.name,
19
- id: job.job_id,
20
- queue: job.queue_name,
21
- params: job.arguments,
22
- executions_count: job.executions,
23
- exception_executions: job.exception_executions,
24
- enqueued_at: job.enqueued_at,
25
- locale: job.locale,
26
- duration: job.enqueued_at.present? ? UnifiedLogger.current_time - job.enqueued_at.in_time_zone : "unknown"
23
+ log_type: :job,
24
+ timestamp: UnifiedLogger.formatted_time,
25
+ class_name: class_name,
26
+ id: id,
27
+ queue: queue,
28
+ params: params,
29
+ retry_count: retry_count,
30
+ enqueued_at: enqueued_at,
31
+ duration: started ? UnifiedLogger.current_time - started : 0,
32
+ queue_duration: enqueued_time && started ? started - enqueued_time : nil,
33
+ thread_id: Thread.current.object_id,
34
+ process_id: Process.pid
27
35
  }
28
- log[:custom] = UnifiedLogger::Logger.fetch_and_reset_custom_logs if UnifiedLogger::Logger.custom_logs.any?
36
+ log.merge!(extra) if extra.any?
37
+ log.compact!
38
+
39
+ log[:logs] = Logger.fetch_and_reset_logs if Logger.logs.any?
40
+ log.merge!(Logger.fetch_and_reset_extra_log_fields) if Logger.extra_log_fields.any?
29
41
 
30
42
  if $!
31
- log[:exception] = UnifiedLogger::Logger.format_exception($!)
32
- log[:status] = job.executions >= DEFAULT_MAX_RETRIES ? :error : :warn
43
+ log[:exception] = Logger.format_exception($!)
44
+ log[:status] = max_retries && retry_count < max_retries ? :warn : :error
33
45
  else
34
46
  log[:status] = :ok
35
47
  end
36
48
 
37
49
  UnifiedLogger.transform_job_log_callable&.call(log)
38
- UnifiedLogger.current_logger.write(UnifiedLogger::Logger.format(log))
50
+ Logger.write_log(log)
51
+ end
52
+
53
+ def parse_time(value)
54
+ case value
55
+ when Numeric then Time.at(value).utc
56
+ when String then Time.parse(value)
57
+ when Time, ActiveSupport::TimeWithZone then value
58
+ end
39
59
  end
40
60
  end
41
61
  end
@@ -1,9 +1,11 @@
1
1
  module UnifiedLogger
2
2
  class Logger < ::Logger
3
- CUSTOM_LOGS = Concurrent::ThreadLocalVar.new([])
3
+ LOGS = Concurrent::ThreadLocalVar.new([])
4
+ EXTRA_LOG_FIELDS = Concurrent::ThreadLocalVar.new({})
4
5
  SEVERITY_LEVELS = {
5
6
  debug: ::Logger::DEBUG,
6
7
  info: ::Logger::INFO,
8
+ note: ::Logger::Severity::NOTE,
7
9
  warn: ::Logger::WARN,
8
10
  error: ::Logger::ERROR,
9
11
  fatal: ::Logger::FATAL,
@@ -27,6 +29,11 @@ module UnifiedLogger
27
29
  add(::Logger::INFO, message)
28
30
  end
29
31
 
32
+ def note(message = nil, &block)
33
+ message = block.call if message.nil? && block
34
+ add(::Logger::Severity::NOTE, message)
35
+ end
36
+
30
37
  def warn(message = nil, &block)
31
38
  message = block.call if message.nil? && block
32
39
  add(::Logger::WARN, message)
@@ -57,18 +64,33 @@ module UnifiedLogger
57
64
  end
58
65
 
59
66
  class << self
60
- def custom_logs
61
- CUSTOM_LOGS.value
67
+ def logs
68
+ LOGS.value
69
+ end
70
+
71
+ def add(hash)
72
+ EXTRA_LOG_FIELDS.value = EXTRA_LOG_FIELDS.value.merge(hash)
73
+ end
74
+
75
+ def extra_log_fields
76
+ EXTRA_LOG_FIELDS.value
77
+ end
78
+
79
+ def fetch_and_reset_extra_log_fields
80
+ fields = extra_log_fields
81
+ EXTRA_LOG_FIELDS.value = {}
82
+ fields
62
83
  end
63
84
 
64
85
  def reset_thread_logs
65
- CUSTOM_LOGS.value = []
86
+ LOGS.value = []
87
+ EXTRA_LOG_FIELDS.value = {}
66
88
  end
67
89
 
68
- def fetch_and_reset_custom_logs
69
- logs = custom_logs
70
- reset_thread_logs
71
- logs
90
+ def fetch_and_reset_logs
91
+ current = logs
92
+ LOGS.value = []
93
+ current
72
94
  end
73
95
 
74
96
  def trim(data)
@@ -107,8 +129,42 @@ module UnifiedLogger
107
129
  formatter.present? ? formatter.call(filtered_log) : filtered_log.to_json
108
130
  end
109
131
 
132
+ def write_log(log)
133
+ logger = UnifiedLogger.current_logger
134
+ max = UnifiedLogger.config[:max_log_size]
135
+ if log.inspect.length <= max || !log.key?(:logs)
136
+ logger.write(format(log))
137
+ else
138
+ entries = log.delete(:logs)
139
+ logger.write(format(log))
140
+ write_overflow_logs(log[:id], log[:log_type], entries, max, logger)
141
+ end
142
+ end
143
+
110
144
  private
111
145
 
146
+ def write_overflow_logs(id, log_type, entries, max, logger)
147
+ index = 1
148
+ group = []
149
+
150
+ entries.each do |entry|
151
+ candidate = group + [entry]
152
+ overflow = { id: id, log_type: log_type, index: index, logs: candidate }
153
+
154
+ if overflow.inspect.length > max && group.any?
155
+ logger.write(format({ id: id, log_type: log_type, index: index, logs: group }))
156
+ index += 1
157
+ group = [entry]
158
+ else
159
+ group = candidate
160
+ end
161
+ end
162
+
163
+ return if group.empty?
164
+
165
+ logger.write(format({ id: id, log_type: log_type, index: index, logs: group }))
166
+ end
167
+
112
168
  def filter(content)
113
169
  return content unless content.respond_to?(:each)
114
170
 
@@ -132,16 +188,16 @@ module UnifiedLogger
132
188
  return true unless severity >= level
133
189
 
134
190
  severity_symbol = SEVERITY_MAP[severity] || :unknown
135
- append_custom_log(severity_symbol, message)
191
+ append_log(severity_symbol, message)
136
192
  end
137
193
 
138
194
  private
139
195
 
140
- def append_custom_log(severity, message)
196
+ def append_log(severity, message)
141
197
  message = sanitize_log_message(message) if message.is_a?(String)
142
198
  log_hash = { timestamp: UnifiedLogger.formatted_time, severity: severity, message: message }
143
199
 
144
- CUSTOM_LOGS.value = CUSTOM_LOGS.value + [log_hash]
200
+ LOGS.value = LOGS.value + [log_hash]
145
201
  end
146
202
 
147
203
  def sanitize_log_message(text)
@@ -13,7 +13,7 @@ module UnifiedLogger
13
13
  if UnifiedLogger.current_logger.is_a?(UnifiedLogger::Logger) && !silenced?(env["REQUEST_PATH"])
14
14
  log = build_log(started, env, status, headers, response)
15
15
  UnifiedLogger.transform_request_log_callable&.call(log, env)
16
- UnifiedLogger.current_logger.write(UnifiedLogger::Logger.format(log))
16
+ UnifiedLogger::Logger.write_log(log)
17
17
  end
18
18
  end
19
19
 
@@ -36,7 +36,7 @@ module UnifiedLogger
36
36
  timestamp: UnifiedLogger.formatted_time,
37
37
  id: env["action_dispatch.request_id"],
38
38
  ip: env["action_dispatch.remote_ip"].to_s,
39
- controller: path_parameters[:controller],
39
+ controller: path_parameters[:controller]&.camelize&.concat("Controller"),
40
40
  action: path_parameters[:action],
41
41
  request: {
42
42
  path: env["REQUEST_PATH"],
@@ -56,7 +56,8 @@ module UnifiedLogger
56
56
  duration: started ? UnifiedLogger.current_time - started : 0
57
57
  }
58
58
  log[:exception] = UnifiedLogger::Logger.format_exception($!) if $!.present?
59
- log[:custom] = UnifiedLogger::Logger.fetch_and_reset_custom_logs if UnifiedLogger::Logger.custom_logs.any?
59
+ log[:logs] = UnifiedLogger::Logger.fetch_and_reset_logs if UnifiedLogger::Logger.logs.any?
60
+ log.merge!(UnifiedLogger::Logger.fetch_and_reset_extra_log_fields) if UnifiedLogger::Logger.extra_log_fields.any?
60
61
 
61
62
  log
62
63
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Extends Ruby's Logger::Severity to recognize the custom :note level (1.5),
4
+ # sitting between INFO (1) and WARN (2). This patch ensures ALL logger instances
5
+ # (not just UnifiedLogger::Logger) accept :note as a valid level — which is
6
+ # required because Rails applies config.log_level to every logger it creates.
7
+
8
+ Logger::Severity.const_set(:NOTE, 1.5) unless Logger::Severity.const_defined?(:NOTE)
9
+
10
+ if Logger::Severity.respond_to?(:coerce)
11
+ # Ruby 3.3+ / logger gem >= 1.6: patch coerce so level= works for :note
12
+ module UnifiedLoggerSeverityCoerce
13
+ CUSTOM_LEVELS = { "note" => 1.5 }.freeze
14
+
15
+ def coerce(severity)
16
+ if severity.is_a?(Numeric)
17
+ severity
18
+ else
19
+ key = severity.to_s.downcase
20
+ CUSTOM_LEVELS[key] || super
21
+ end
22
+ end
23
+ end
24
+
25
+ Logger::Severity.singleton_class.prepend(UnifiedLoggerSeverityCoerce)
26
+ else
27
+ # Older Ruby: no coerce method, patch level= directly on Logger
28
+ module UnifiedLoggerSeverityLevel
29
+ def level=(severity)
30
+ if severity.is_a?(Numeric)
31
+ @level = severity
32
+ elsif severity.to_s.downcase == "note"
33
+ @level = Logger::Severity::NOTE
34
+ else
35
+ super
36
+ end
37
+ end
38
+ end
39
+
40
+ Logger.prepend(UnifiedLoggerSeverityLevel)
41
+ end
@@ -0,0 +1,40 @@
1
+ require "unified_logger"
2
+
3
+ module UnifiedLogger
4
+ class SidekiqServerMiddleware
5
+ def call(job_instance, job_hash, queue)
6
+ UnifiedLogger::JobLogger.log(**attrs_from(job_instance, job_hash, queue)) { yield } # rubocop:disable Style/ExplicitBlockArgument
7
+ end
8
+
9
+ private
10
+
11
+ def attrs_from(job_instance, job_hash, queue)
12
+ aj = job_hash.key?("wrapped") ? job_hash.dig("args", 0) : nil
13
+ retries = job_hash["retry"]
14
+
15
+ {
16
+ class_name: aj&.[]("job_class") || job_hash["wrapped"] || job_instance.class.name,
17
+ id: aj&.[]("job_id") || job_hash["jid"],
18
+ queue: queue,
19
+ params: aj&.[]("arguments") || job_hash["args"],
20
+ retry_count: job_hash["retry_count"].to_i,
21
+ max_retries: resolve_max_retries(retries),
22
+ enqueued_at: aj&.[]("enqueued_at") || job_hash["enqueued_at"]
23
+ }.compact
24
+ end
25
+
26
+ def resolve_max_retries(retries)
27
+ return retries if retries.is_a?(Integer)
28
+
29
+ retries == false ? 0 : 25
30
+ end
31
+ end
32
+ end
33
+
34
+ if defined?(Sidekiq)
35
+ Sidekiq.configure_server do |config|
36
+ config.server_middleware do |chain|
37
+ chain.add UnifiedLogger::SidekiqServerMiddleware
38
+ end
39
+ end
40
+ end
@@ -1,3 +1,3 @@
1
1
  module UnifiedLogger
2
- VERSION = "0.1.5".freeze
2
+ VERSION = "0.1.7".freeze
3
3
  end
@@ -11,6 +11,7 @@ require "json"
11
11
  require "logger"
12
12
 
13
13
  require_relative "unified_logger/version"
14
+ require_relative "unified_logger/severity"
14
15
  require_relative "unified_logger/logger"
15
16
  require_relative "unified_logger/request_logger"
16
17
  require_relative "unified_logger/job_logger"
@@ -20,6 +21,7 @@ module UnifiedLogger
20
21
 
21
22
  DEFAULTS = {
22
23
  max_log_field_size: 2048,
24
+ max_log_size: 10_000,
23
25
  filter_params: %i[passw secret token crypt salt certificate otp ssn set-cookie http_authorization http_cookie pin],
24
26
  auto_insert_middleware: true,
25
27
  silence_paths: []
@@ -55,7 +57,8 @@ module UnifiedLogger
55
57
  end
56
58
 
57
59
  delegate :trim, :format, :format_exception,
58
- :custom_logs, :fetch_and_reset_custom_logs, :reset_thread_logs,
60
+ :logs, :fetch_and_reset_logs, :reset_thread_logs,
61
+ :add, :extra_log_fields, :fetch_and_reset_extra_log_fields,
59
62
  to: :"UnifiedLogger::Logger"
60
63
  end
61
64
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unified_logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcovecchio
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-03-24 00:00:00.000000000 Z
10
+ date: 2026-03-31 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -75,6 +75,8 @@ files:
75
75
  - lib/unified_logger/logger.rb
76
76
  - lib/unified_logger/railtie.rb
77
77
  - lib/unified_logger/request_logger.rb
78
+ - lib/unified_logger/severity.rb
79
+ - lib/unified_logger/sidekiq.rb
78
80
  - lib/unified_logger/version.rb
79
81
  homepage: https://github.com/marcovecchio/unified_logger
80
82
  licenses: