Class: Doing::Logger
Overview
Log adapter
Constant Summary collapse
- TOPIC_WIDTH =
12- LOG_LEVELS =
{ debug: ::Logger::DEBUG, info: ::Logger::INFO, warn: ::Logger::WARN, error: ::Logger::ERROR }.freeze
- COUNT_KEYS =
%i[ added added_tags archived autotag completed completed_archived deleted moved removed_tags rotated skipped updated exported ].freeze
Instance Attribute Summary collapse
-
#level ⇒ Object
readonly
Returns the current log level (debug, info, warn, error).
-
#logdev ⇒ Object
writeonly
Sets the log device.
-
#max_length ⇒ Object
writeonly
Max length of log messages (truncate in middle).
-
#messages ⇒ Object
readonly
Returns the value of attribute messages.
-
#results ⇒ Object
readonly
Returns the value of attribute results.
Instance Method Summary collapse
-
#abort_with(topic, message = nil, &block) ⇒ Object
Print an error message and immediately abort the process.
- #adjust_verbosity(options = {}) ⇒ Object
-
#all_benchmark_stats ⇒ Object
Get all benchmark statistics sorted by duration.
- #benchmark(key, state) ⇒ Object
-
#benchmark_stats(key) ⇒ Object
Get benchmark statistics for a specific key.
-
#benchmark_summary ⇒ Object
Generate a compact benchmark summary.
- #count(key, level: :info, count: 1, tag: nil, message: nil) ⇒ Object
-
#debug(topic, message = nil, &block) ⇒ Object
Print a debug message.
-
#error(topic, message = nil, &block) ⇒ Object
Print an error message.
-
#formatted_topic(topic, colon: false) ⇒ Object
Format the topic.
-
#info(topic, message = nil, &block) ⇒ Object
Print a message.
-
#initialize(level = :info) ⇒ Logger
constructor
Create a new instance of a log writer.
- #log_benchmarks ⇒ Object
- #log_change(tags_added: [], tags_removed: [], count: 1, item: nil, single: false) ⇒ Object
-
#log_level=(level = 'info') ⇒ Object
Set the log level on the writer.
-
#log_now(level, topic, message = nil, &block) ⇒ Object
Log to console immediately instead of writing messages on exit.
-
#measure(key, &_block) ⇒ Object
Measure execution time of a block with automatic start/finish.
-
#output_results ⇒ Object
Output registers based on log level.
-
#restore_level ⇒ Object
Restore temporary level.
-
#temp_level(level) ⇒ Object
Set log level temporarily.
-
#warn(topic, message = nil, &block) ⇒ Object
Print a message.
-
#write(level_of_message, topic, message = nil, &block) ⇒ Boolean
Log a message.
Constructor Details
#initialize(level = :info) ⇒ Logger
Create a new instance of a log writer
49 50 51 52 53 54 55 56 57 58 |
# File 'lib/doing/logger.rb', line 49 def initialize(level = :info) @messages = [] @counters = {} COUNT_KEYS.each { |key| @counters[key] = { tag: [], count: 0 } } @results = [] @logdev = $stderr @max_length = TTY::Screen.columns - 5 || 85 self.log_level = level @prev_level = level end |
Instance Attribute Details
#level ⇒ Object (readonly)
Returns the current log level (debug, info, warn, error)
15 16 17 |
# File 'lib/doing/logger.rb', line 15 def level @level end |
#logdev=(value) ⇒ Object (writeonly)
Sets the log device
9 10 11 |
# File 'lib/doing/logger.rb', line 9 def logdev=(value) @logdev = value end |
#max_length=(value) ⇒ Object (writeonly)
Max length of log messages (truncate in middle)
12 13 14 |
# File 'lib/doing/logger.rb', line 12 def max_length=(value) @max_length = value end |
#messages ⇒ Object (readonly)
Returns the value of attribute messages.
17 18 19 |
# File 'lib/doing/logger.rb', line 17 def @messages end |
#results ⇒ Object (readonly)
Returns the value of attribute results.
17 18 19 |
# File 'lib/doing/logger.rb', line 17 def results @results end |
Instance Method Details
#abort_with(topic, message = nil, &block) ⇒ Object
Print an error message and immediately abort the process
187 188 189 190 |
# File 'lib/doing/logger.rb', line 187 def abort_with(topic, = nil, &block) error(topic, , &block) abort end |
#adjust_verbosity(options = {}) ⇒ Object
100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/doing/logger.rb', line 100 def adjust_verbosity( = {}) if [:log_level] self.log_level = [:log_level].to_sym elsif [:quiet] self.log_level = :error elsif [:verbose] || [:debug] self.log_level = :debug end log_now :debug, 'Logging at level:', @level.to_s # log_now :debug, 'Doing Version:', Doing::VERSION end |
#all_benchmark_stats ⇒ Object
Get all benchmark statistics sorted by duration
304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/doing/logger.rb', line 304 def all_benchmark_stats return [] unless @benchmarks @benchmarks.map do |key, timers| next unless timers[:start] && timers[:finish] { key: key, duration: (timers[:finish] - timers[:start]).round(4), start_time: timers[:start].round(4), end_time: timers[:finish].round(4) } end.compact.sort_by { |stats| -stats[:duration] } end |
#benchmark(key, state) ⇒ Object
270 271 272 273 274 275 276 277 278 279 |
# File 'lib/doing/logger.rb', line 270 def benchmark(key, state) return unless ENV['DOING_BENCHMARK'] # Pre-allocate benchmarks hash to avoid repeated allocation @benchmarks ||= {} # Use direct assignment instead of ||= for better performance @benchmarks[key] = { start: nil, finish: nil } if @benchmarks[key].nil? @benchmarks[key][state] = Process.clock_gettime(Process::CLOCK_MONOTONIC) end |
#benchmark_stats(key) ⇒ Object
Get benchmark statistics for a specific key
292 293 294 295 296 297 298 299 300 301 |
# File 'lib/doing/logger.rb', line 292 def benchmark_stats(key) return nil unless @benchmarks.dig(key, :start) && @benchmarks.dig(key, :finish) { key: key, duration: (@benchmarks[key][:finish] - @benchmarks[key][:start]).round(4), start_time: @benchmarks[key][:start].round(4), end_time: @benchmarks[key][:finish].round(4) } end |
#benchmark_summary ⇒ Object
Generate a compact benchmark summary
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 |
# File 'lib/doing/logger.rb', line 320 def benchmark_summary return unless ENV['DOING_BENCHMARK'] && @benchmarks stats = all_benchmark_stats return if stats.empty? total_duration = stats.find { |s| s[:key] == :total }&.dig(:duration) || 0 return if total_duration <= 0 output = [] output << "Benchmark Summary (Total: #{total_duration.round(4)}s):" stats.reject { |s| s[:key] == :total }.each do |stat| percentage = (stat[:duration] / total_duration * 100).round(1) output << " #{stat[:key]}: #{stat[:duration]}s (#{percentage}%)" end output.join("\n") end |
#count(key, level: :info, count: 1, tag: nil, message: nil) ⇒ Object
112 113 114 115 116 117 118 119 |
# File 'lib/doing/logger.rb', line 112 def count(key, level: :info, count: 1, tag: nil, message: nil) raise ArgumentError, 'invalid counter key' unless COUNT_KEYS.include?(key) @counters[key][:count] += count @counters[key][:tag].concat(tag).sort.uniq unless tag.nil? @counters[key][:level] ||= level @counters[key][:message] ||= end |
#debug(topic, message = nil, &block) ⇒ Object
Print a debug message
129 130 131 |
# File 'lib/doing/logger.rb', line 129 def debug(topic, = nil, &block) write(:debug, topic, , &block) end |
#error(topic, message = nil, &block) ⇒ Object
Print an error message
171 172 173 |
# File 'lib/doing/logger.rb', line 171 def error(topic, = nil, &block) write(:error, topic, , &block) end |
#formatted_topic(topic, colon: false) ⇒ Object
Format the topic
202 203 204 205 206 207 208 209 210 |
# File 'lib/doing/logger.rb', line 202 def formatted_topic(topic, colon: false) if colon "#{topic}: ".rjust(TOPIC_WIDTH) elsif topic =~ /:$/ "#{topic} ".rjust(TOPIC_WIDTH) else "#{topic} " end end |
#info(topic, message = nil, &block) ⇒ Object
Print a message
143 144 145 |
# File 'lib/doing/logger.rb', line 143 def info(topic, = nil, &block) write(:info, topic, , &block) end |
#log_benchmarks ⇒ Object
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 |
# File 'lib/doing/logger.rb', line 339 def log_benchmarks return unless ENV['DOING_BENCHMARK'] && @benchmarks # Cache screen width to avoid repeated calls screen_width = TTY::Screen.columns return if screen_width <= 0 beginning = @benchmarks[:total]&.dig(:start) ending = @benchmarks[:total]&.dig(:finish) return unless beginning && ending total = ending - beginning return if total <= 0 factor = screen_width.to_f / total cols = Array.new(screen_width, ' ') output = [] # Pre-allocate colors array to avoid repeated allocation colors = %w[bgred bggreen bgyellow bgblue bgmagenta bgcyan bgwhite boldbgred boldbggreen boldbgyellow boldbgblue boldbgwhite] color_count = colors.size # Sort benchmarks once and cache the result sorted_benchmarks = @benchmarks.reject { |k, _| k == :total } .select { |_, timers| timers[:finish] && timers[:start] } .sort_by { |_, timers| timers[:start] } sorted_benchmarks.each_with_index do |(key, timers), idx| color_name = colors[idx % color_count] fg_color = idx < 7 ? Color.boldblack : Color.boldwhite color = Color.send(color_name) + fg_color start_pos = ((timers[:start] - beginning) * factor).floor finish_pos = ((timers[:finish] - beginning) * factor).ceil # Ensure positions are within bounds start_pos = [start_pos, 0].max finish_pos = [finish_pos, screen_width - 1].min if start_pos < finish_pos cols.fill("#{color}-", start_pos..finish_pos) cols[start_pos] = "#{color}|" cols[finish_pos] = "#{color}|" end duration = (timers[:finish] - timers[:start]).round(4) output << "#{color}#{key}#{Color.default}: #{duration}" end # Output all messages at once to reduce I/O overhead output.each { |msg| $stdout.puts (:debug, 'Benchmark:', msg) } $stdout.puts (:debug, 'Benchmark:', "Total: #{total.round(4)}") $stdout.puts cols.join + Color.reset end |
#log_change(tags_added: [], tags_removed: [], count: 1, item: nil, single: false) ⇒ Object
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 |
# File 'lib/doing/logger.rb', line 395 def log_change(tags_added: [], tags_removed: [], count: 1, item: nil, single: false) if .empty? && .empty? count(:skipped, level: :debug, message: '%count %items with no change', count: count) else if .empty? count(:skipped, level: :debug, message: 'no tags added to %count %items') elsif single && item elapsed = if item && .include?('done') item.interval ? " (#{item.interval&.time_string(format: :dhm)})" : '' else '' end added = . info('Tagged:', %(added #{.count == 1 ? 'tag' : 'tags'} #{added}#{elapsed} to #{item.title})) else count(:added_tags, level: :info, tag: , message: '%tags added to %count %items') end if .empty? count(:skipped, level: :debug, message: 'no tags removed from %count %items') elsif single && item removed = . info('Untagged:', %(removed #{.count == 1 ? 'tag' : 'tags'} #{removed} from #{item.title})) else count(:removed_tags, level: :info, tag: , message: '%tags removed from %count %items') end end end |
#log_level=(level = 'info') ⇒ Object
Set the log level on the writer
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/doing/logger.rb', line 67 def log_level=(level = 'info') level = level.to_s level = case level when /^[e0]/i :error when /^[w1]/i :warn when /^[d3]/i :debug else :info end @level = level end |
#log_now(level, topic, message = nil, &block) ⇒ Object
Log to console immediately instead of writing messages on exit
241 242 243 244 245 246 247 248 249 |
# File 'lib/doing/logger.rb', line 241 def log_now(level, topic, = nil, &block) return false unless (level) if @logdev == $stdout @logdev.puts (topic, , &block) else @logdev.puts (level, topic, , &block) end end |
#measure(key, &_block) ⇒ Object
Measure execution time of a block with automatic start/finish
282 283 284 285 286 287 288 289 |
# File 'lib/doing/logger.rb', line 282 def measure(key, &_block) return yield unless ENV['DOING_BENCHMARK'] benchmark(key, :start) result = yield benchmark(key, :finish) result end |
#output_results ⇒ Object
Output registers based on log level
256 257 258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/doing/logger.rb', line 256 def output_results total_counters results = @results.select { |msg| (msg[:level]) }.uniq if @logdev == $stdout $stdout.print results.map { |res| res[:message].uncolor }.join("\n") $stdout.puts else results.each do |msg| @logdev.puts (msg[:level], msg[:message]) end end end |
#restore_level ⇒ Object
Restore temporary level
93 94 95 96 97 98 |
# File 'lib/doing/logger.rb', line 93 def restore_level return if @prev_level.nil? || @prev_level == @log_level self.log_level = @prev_level @prev_level = nil end |
#temp_level(level) ⇒ Object
Set log level temporarily
85 86 87 88 89 90 |
# File 'lib/doing/logger.rb', line 85 def temp_level(level) return if level.nil? || level.to_sym == @log_level @prev_level = log_level.dup @log_level = level.to_sym end |
#warn(topic, message = nil, &block) ⇒ Object
Print a message
157 158 159 |
# File 'lib/doing/logger.rb', line 157 def warn(topic, = nil, &block) write(:warn, topic, , &block) end |
#write(level_of_message, topic, message = nil, &block) ⇒ Boolean
Log a message.
228 229 230 231 |
# File 'lib/doing/logger.rb', line 228 def write(, topic, = nil, &block) @results << { level: , message: (topic, , &block) } true end |