loading
Generated 2025-10-09T14:21:48+00:00

All Files ( 100.0% covered at 18.84 hits/line )

34 files in total.
528 relevant lines, 528 lines covered and 0 lines missed. ( 100.0% )
59 total branches, 59 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
lib/cogger.rb 100.00 % 24 13 13 0 1.23 100.00 % 0 0 0
lib/cogger/configuration.rb 100.00 % 70 15 15 0 20.00 100.00 % 2 2 0
lib/cogger/entry.rb 100.00 % 74 28 28 0 36.36 100.00 % 10 10 0
lib/cogger/formatters/abstract.rb 100.00 % 44 19 19 0 70.95 100.00 % 0 0 0
lib/cogger/formatters/color.rb 100.00 % 29 15 15 0 22.20 100.00 % 0 0 0
lib/cogger/formatters/crash.rb 100.00 % 35 16 16 0 4.50 100.00 % 0 0 0
lib/cogger/formatters/emoji.rb 100.00 % 14 6 6 0 9.83 100.00 % 0 0 0
lib/cogger/formatters/json.rb 100.00 % 30 16 16 0 5.69 100.00 % 0 0 0
lib/cogger/formatters/parsers/abstract.rb 100.00 % 31 15 15 0 21.27 100.00 % 0 0 0
lib/cogger/formatters/parsers/combined.rb 100.00 % 24 11 11 0 27.91 100.00 % 0 0 0
lib/cogger/formatters/parsers/element.rb 100.00 % 41 17 17 0 19.47 100.00 % 0 0 0
lib/cogger/formatters/parsers/emoji.rb 100.00 % 39 17 17 0 16.94 100.00 % 0 0 0
lib/cogger/formatters/parsers/key.rb 100.00 % 49 20 20 0 31.95 100.00 % 0 0 0
lib/cogger/formatters/parsers/position.rb 100.00 % 37 14 14 0 9.07 100.00 % 2 2 0
lib/cogger/formatters/property.rb 100.00 % 35 18 18 0 6.50 100.00 % 0 0 0
lib/cogger/formatters/sanitizers/escape.rb 100.00 % 42 14 14 0 5.86 100.00 % 2 2 0
lib/cogger/formatters/sanitizers/filter.rb 100.00 % 13 6 6 0 36.67 100.00 % 2 2 0
lib/cogger/formatters/sanitizers/format_time.rb 100.00 % 16 7 7 0 74.57 100.00 % 2 2 0
lib/cogger/formatters/simple.rb 100.00 % 26 13 13 0 4.62 100.00 % 0 0 0
lib/cogger/formatters/transformers/color.rb 100.00 % 36 18 18 0 25.61 100.00 % 4 4 0
lib/cogger/formatters/transformers/emoji.rb 100.00 % 30 15 15 0 11.07 100.00 % 4 4 0
lib/cogger/formatters/transformers/key.rb 100.00 % 10 4 4 0 49.25 100.00 % 2 2 0
lib/cogger/hub.rb 100.00 % 123 51 51 0 12.90 100.00 % 6 6 0
lib/cogger/level.rb 100.00 % 17 8 8 0 9.75 100.00 % 2 2 0
lib/cogger/program.rb 100.00 % 10 4 4 0 63.25 100.00 % 0 0 0
lib/cogger/rack/logger.rb 100.00 % 51 22 22 0 4.27 100.00 % 0 0 0
lib/cogger/refines/log_device.rb 100.00 % 23 12 12 0 2.50 100.00 % 3 3 0
lib/cogger/refines/logger.rb 100.00 % 16 7 7 0 1.00 100.00 % 0 0 0
lib/cogger/registry.rb 100.00 % 106 41 41 0 30.85 100.00 % 0 0 0
lib/cogger/tag.rb 100.00 % 40 19 19 0 17.11 100.00 % 8 8 0
lib/cogger/time/clock.rb 100.00 % 8 3 3 0 4.67 100.00 % 0 0 0
lib/cogger/time/range.rb 100.00 % 13 3 3 0 1.00 100.00 % 0 0 0
lib/cogger/time/span.rb 100.00 % 58 32 32 0 3.63 100.00 % 5 5 0
lib/cogger/time/unit.rb 100.00 % 16 9 9 0 3.44 100.00 % 5 5 0

lib/cogger.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "zeitwerk"
  3. 1 Zeitwerk::Loader.new.then do |loader|
  4. 1 loader.inflector.inflect "json" => "JSON", "range" => "RANGE"
  5. 1 loader.tag = File.basename __FILE__, ".rb"
  6. 1 loader.push_dir __dir__
  7. 1 loader.setup
  8. end
  9. # Main namespace.
  10. 1 module Cogger
  11. 1 extend Registry
  12. 1 DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%L%:z"
  13. 1 LEVELS = %w[debug info warn error fatal unknown].freeze
  14. 1 def self.loader registry = Zeitwerk::Registry
  15. 4 @loader ||= registry.loaders.each.find { |loader| loader.tag == File.basename(__FILE__, ".rb") }
  16. end
  17. 1 def self.new(...) = Hub.new(...)
  18. end

lib/cogger/configuration.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "logger"
  4. 1 require "refinements/array"
  5. 1 module Cogger
  6. # Defines the default configuration for all pipes.
  7. 1 Configuration = Data.define(
  8. :id,
  9. :io,
  10. :level,
  11. :formatter,
  12. :datetime_format,
  13. :tags,
  14. :header,
  15. :mode,
  16. :age,
  17. :size,
  18. :suffix,
  19. :entry,
  20. :logger,
  21. :mutex
  22. ) do
  23. 1 using Refinements::Array
  24. 1 def initialize id: Program.call,
  25. io: $stdout,
  26. level: Level.call,
  27. formatter: Formatters::Emoji.new,
  28. datetime_format: DATETIME_FORMAT,
  29. tags: Core::EMPTY_ARRAY,
  30. header: true,
  31. mode: false,
  32. age: nil,
  33. size: 1_048_576,
  34. suffix: "%Y-%m-%d",
  35. entry: Entry,
  36. logger: Logger,
  37. mutex: Mutex.new
  38. 158 super.tap { tags.freeze }
  39. end
  40. 52 then: 7 else: 44 def entag(other = nil) = other ? tags.including(other) : tags
  41. 1 def to_logger
  42. 71 logger.new io,
  43. age,
  44. size,
  45. progname: id,
  46. level:,
  47. formatter:,
  48. datetime_format:,
  49. skip_header: skip_header?,
  50. binmode: mode,
  51. shift_period_suffix: suffix
  52. end
  53. 1 def inspect
  54. 8 "#<#{self.class} @id=#{id}, @io=#{io.class}, @level=#{level}, " \
  55. "@formatter=#{formatter.class}, @datetime_format=#{datetime_format.inspect}, " \
  56. "@tags=#{tags.inspect}, @header=#{header}, @mode=#{mode}, @age=#{age}, @size=#{size}, " \
  57. "@suffix=#{suffix.inspect}, @entry=#{entry}, @logger=#{logger}>"
  58. end
  59. 1 private
  60. 1 def skip_header? = formatter == :json || formatter.is_a?(Formatters::JSON) || !header
  61. end
  62. end

lib/cogger/entry.rb

100.0% lines covered

100.0% branches covered

28 relevant lines. 28 lines covered and 0 lines missed.
10 total branches, 10 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Cogger
  4. # Defines a log entry which can be formatted for output.
  5. 1 Entry = Data.define :id, :level, :at, :message, :tags, :datetime_format, :payload do
  6. 1 def self.for(message = nil, **payload, &)
  7. 91 then: 21 else: 70 content = block_given? ? yield : message
  8. 91 new id: payload.delete(:id) || Program.call,
  9. 91 level: (payload.delete(:level) || "INFO").upcase,
  10. at: payload.delete(:at) || ::Time.now,
  11. message: sanitize!(content, payload),
  12. tags: Array(payload.delete(:tags)),
  13. datetime_format: payload.delete(:datetime_format) || DATETIME_FORMAT,
  14. payload:
  15. end
  16. 1 def self.for_crash message, error, id:
  17. 10 new id:,
  18. level: "FATAL",
  19. message:,
  20. payload: {
  21. error_message: error.message,
  22. error_class: error.class,
  23. backtrace: error.backtrace
  24. }
  25. end
  26. 1 def self.sanitize! content, payload
  27. 91 then: 3 body = if content.is_a? Hash
  28. 6 content.delete(:message).tap { payload.merge! content }
  29. else: 88 else
  30. 88 content
  31. end
  32. 91 then: 52 if body.is_a? String
  33. 52 body.encode "UTF-8", invalid: :replace, undef: :replace, replace: "?"
  34. else: 39 else
  35. 39 body
  36. end
  37. end
  38. 1 private_class_method :sanitize!
  39. 1 def initialize id: Program.call,
  40. level: "INFO",
  41. at: ::Time.now,
  42. message: nil,
  43. tags: Core::EMPTY_ARRAY,
  44. datetime_format: DATETIME_FORMAT,
  45. payload: Core::EMPTY_HASH
  46. 108 super
  47. end
  48. 1 def attributes = {id:, level:, at:, message:, **payload}
  49. 1 def tagged_attributes tagger: Tag
  50. 22 computed_tags = tagger.for(*tags)
  51. 22 then: 15 else: 7 return attributes if computed_tags.empty?
  52. 7 {id:, level:, at:, message:, **computed_tags.to_h, **payload}
  53. end
  54. 1 def tagged tagger: Tag
  55. 66 attributes.tap do |pairs|
  56. 66 computed_tags = tagger.for(*tags)
  57. 66 else: 65 then: 1 pairs[:message] = "#{computed_tags} #{pairs[:message]}" unless computed_tags.empty?
  58. end
  59. end
  60. end
  61. end

lib/cogger/formatters/abstract.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Formatters
  4. # An abstract class with common/shared functionality.
  5. 1 class Abstract
  6. 1 NEW_LINE = "\n"
  7. SANITIZERS = {
  8. 1 escape: Sanitizers::Escape.new,
  9. filter: Sanitizers::Filter,
  10. format_time: Sanitizers::FormatTime
  11. }.freeze
  12. 1 def initialize sanitizers: SANITIZERS
  13. 109 @sanitizers = sanitizers
  14. end
  15. 1 def call(*)
  16. 1 fail NoMethodError,
  17. "`#{self.class}##{__method__} #{method(__method__).parameters}` must be implemented."
  18. end
  19. 1 protected
  20. 1 def sanitize entry, method
  21. 84 entry.public_send(method).tap do |attributes|
  22. 84 filter attributes
  23. 506 attributes.transform_values! { |value| format_time value, format: entry.datetime_format }
  24. end
  25. end
  26. 45 def escape(...) = (@escape ||= sanitizers.fetch(__method__)).call(...)
  27. 85 def filter(...) = (@filter ||= sanitizers.fetch(__method__)).call(...)
  28. 423 def format_time(...) = (@format_time ||= sanitizers.fetch(__method__)).call(...)
  29. 1 private
  30. 1 attr_reader :sanitizers
  31. end
  32. end
  33. end

lib/cogger/formatters/color.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Formatters
  4. # Formats by color.
  5. 1 class Color < Abstract
  6. 1 TEMPLATE = "<dynamic>[%<id>s]</dynamic> %<message:dynamic>s"
  7. 1 def initialize template = TEMPLATE, parser: Parsers::Combined.new
  8. 63 super()
  9. 63 @template = template
  10. 63 @parser = parser
  11. end
  12. 1 def call(*input)
  13. 45 *, entry = input
  14. 45 attributes = sanitize entry, :tagged
  15. 45 format(parse(attributes[:level]), attributes).tap(&:strip!) << NEW_LINE
  16. end
  17. 1 private
  18. 1 attr_reader :template, :parser
  19. 1 def parse(level) = parser.call template, level
  20. end
  21. end
  22. end

lib/cogger/formatters/crash.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Formatters
  4. # Formats fatal crashes.
  5. 1 class Crash < Abstract
  6. 1 TEMPLATE = <<~CONTENT
  7. <dynamic>[%<id>s] [%<level>s] [%<at>s] Crash!
  8. %<message>s
  9. %<error_message>s (%<error_class>s)
  10. %<backtrace>s</dynamic>
  11. CONTENT
  12. 1 def initialize template = TEMPLATE, parser: Parsers::Combined.new
  13. 9 super()
  14. 9 @template = template
  15. 9 @parser = parser
  16. end
  17. 1 def call(*input)
  18. 9 *, entry = input
  19. 9 attributes = sanitize entry, :tagged
  20. 9 attributes[:backtrace] = %( #{attributes[:backtrace].join "\n "})
  21. 9 format(parse(attributes[:level]), attributes) << NEW_LINE
  22. end
  23. 1 private
  24. 1 attr_reader :template, :parser
  25. 1 def parse(level) = parser.call template, level
  26. end
  27. end
  28. end

lib/cogger/formatters/emoji.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Formatters
  4. # Formats by emoji and color.
  5. 1 class Emoji < Color
  6. 1 TEMPLATE = "%<emoji:dynamic>s <dynamic>[%<id>s]</dynamic> %<message:dynamic>s"
  7. 1 def initialize(template = TEMPLATE, ...)
  8. 54 super
  9. end
  10. end
  11. end
  12. end

lib/cogger/formatters/json.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "json"
  4. 1 module Cogger
  5. 1 module Formatters
  6. # Formats as JSON output.
  7. 1 class JSON < Abstract
  8. 1 TEMPLATE = nil
  9. 1 def initialize template = TEMPLATE, parser: Parsers::Position.new
  10. 16 super()
  11. 16 @template = template
  12. 16 @parser = parser
  13. end
  14. 1 def call(*input)
  15. 11 *, entry = input
  16. 11 attributes = sanitize(entry, :tagged_attributes).tap(&:compact!)
  17. 11 parser.call(template, attributes).to_json << NEW_LINE
  18. end
  19. 1 private
  20. 1 attr_reader :template, :parser
  21. end
  22. end
  23. end

lib/cogger/formatters/parsers/abstract.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Formatters
  4. 1 module Parsers
  5. # An abstract class with common functionality.
  6. 1 class Abstract
  7. 1 TRANSFORMERS = {color: Transformers::Color.new, emoji: Transformers::Emoji.new}.freeze
  8. 1 def initialize registry: Cogger, transformers: TRANSFORMERS, expressor: Regexp
  9. 41 @registry = registry
  10. 41 @transformers = transformers
  11. 41 @expressor = expressor
  12. end
  13. 1 def call(_template, **)
  14. 1 fail NoMethodError,
  15. "`#{self.class}##{__method__} #{method(__method__).parameters}` must be implemented."
  16. end
  17. 1 protected
  18. 1 attr_reader :registry, :transformers, :expressor
  19. 141 def transform_color(...) = (@tranform_color ||= transformers.fetch(:color)).call(...)
  20. 45 def transform_emoji(...) = (@transform_emoji ||= transformers.fetch(:emoji)).call(...)
  21. end
  22. end
  23. end
  24. end

lib/cogger/formatters/parsers/combined.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Formatters
  4. 1 module Parsers
  5. # Parses template literals, emojis, and keys for specific and dynamic colors.
  6. 1 class Combined
  7. 1 STEPS = [Element.new, Emoji.new, Key.new].freeze # Order matters.
  8. 1 def initialize steps: STEPS
  9. 74 @steps = steps
  10. end
  11. 1 def call template, level
  12. 224 steps.reduce(template.dup) { |modification, step| step.call modification, level }
  13. end
  14. 1 private
  15. 1 attr_reader :steps
  16. end
  17. end
  18. end
  19. end

lib/cogger/formatters/parsers/element.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Formatters
  4. 1 module Parsers
  5. # Parses template elements for specific and dynamic colors.
  6. 1 class Element < Abstract
  7. 1 PATTERN = %r(
  8. < # Tag open start.
  9. (?<directive>\w+) # Tag open name.
  10. > # Tag open end.
  11. (?<content>.+?) # Content.
  12. </ # Tag close start.
  13. \w+ # Tag close.
  14. > # Tag close end.
  15. )mx
  16. 1 def initialize pattern: PATTERN
  17. 8 super()
  18. 8 @pattern = pattern
  19. end
  20. 1 def call template, level
  21. 63 mutate template, level
  22. 63 template
  23. end
  24. 1 private
  25. 1 attr_reader :pattern
  26. 1 def mutate template, level
  27. 63 template.gsub! pattern do
  28. 58 captures = expressor.last_match.named_captures
  29. 58 transform_color captures["content"], captures["directive"], level
  30. end
  31. end
  32. end
  33. end
  34. end
  35. end

lib/cogger/formatters/parsers/emoji.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Formatters
  4. 1 module Parsers
  5. # Parses template emojis for specific and dynamic colors.
  6. 1 class Emoji < Abstract
  7. 1 PATTERN = /
  8. %< # Start.
  9. (?<key>emoji) # Key.
  10. : # Delimiter.
  11. (?<directive>\w+) # Directive.
  12. >s # End.
  13. /x
  14. 1 def initialize pattern: PATTERN
  15. 5 super()
  16. 5 @pattern = pattern
  17. end
  18. 1 def call template, level
  19. 60 mutate template, level
  20. 60 template
  21. end
  22. 1 private
  23. 1 attr_reader :pattern
  24. 1 def mutate template, level
  25. 60 template.gsub! pattern do
  26. 44 captures = expressor.last_match.named_captures
  27. 44 transform_emoji captures["key"], captures["directive"], level
  28. end
  29. end
  30. end
  31. end
  32. end
  33. end

lib/cogger/formatters/parsers/key.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Cogger
  4. 1 module Formatters
  5. 1 module Parsers
  6. # Parses template for specific and dynamic keys (i.e. string format specifiers).
  7. 1 class Key < Abstract
  8. 1 PATTERN = /
  9. % # Start.
  10. (?<flag>[\s#+-0*])? # Optional flag.
  11. \.? # Optional precision.
  12. (?<width>\d+)? # Optional width.
  13. < # Reference start.
  14. (?<key>\w+) # Key.
  15. : # Delimiter.
  16. (?<directive>\w+) # Directive.
  17. > # Reference end.
  18. (?<specifier>[ABEGXabcdefgiopsux]) # Specifier.
  19. /x
  20. 1 def initialize pattern: PATTERN
  21. 27 super()
  22. 27 @pattern = pattern
  23. end
  24. 1 def call template, level
  25. 82 mutate template, level
  26. 82 template
  27. end
  28. 1 private
  29. 1 attr_reader :pattern
  30. 1 def mutate template, level
  31. 82 template.gsub! pattern do |match|
  32. 82 captures = expressor.last_match.named_captures
  33. 82 directive = captures["directive"]
  34. 82 match.sub! ":#{directive}", Core::EMPTY_STRING
  35. 82 transform_color match, directive, level
  36. end
  37. end
  38. end
  39. end
  40. end
  41. end

lib/cogger/formatters/parsers/position.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Formatters
  4. 1 module Parsers
  5. # Parses template and reorders attributes based on template key positions.
  6. 1 class Position
  7. 1 PATTERN = /
  8. % # Start.
  9. ? # Flag, width, or precision.
  10. < # Reference start.
  11. (?<name>\w+) # Name.
  12. (?::\w+)? # Optional delimiter and directive.
  13. > # Reference end.
  14. ? # Specifier.
  15. /x
  16. 1 def initialize pattern: PATTERN
  17. 33 @pattern = pattern
  18. end
  19. 1 def call template, attributes
  20. 28 else: 11 then: 17 return attributes unless String(template).match? pattern
  21. 11 keys = scan template
  22. 11 attributes.slice(*keys).merge!(attributes.except(*keys))
  23. end
  24. 1 private
  25. 1 attr_reader :pattern
  26. 35 def scan(template) = template.scan(pattern).map { |match| match.first.to_sym }
  27. end
  28. end
  29. end
  30. end

lib/cogger/formatters/property.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Cogger
  4. 1 module Formatters
  5. # Formats as key=value output.
  6. 1 class Property < Abstract
  7. 1 TEMPLATE = nil
  8. 1 def initialize template = TEMPLATE, parser: Parsers::Position.new
  9. 9 super()
  10. 9 @template = template
  11. 9 @parser = parser
  12. end
  13. 1 def call(*input)
  14. 9 *, entry = input
  15. 9 attributes = sanitize(entry, :tagged_attributes).tap(&:compact!)
  16. 9 concat(attributes).chop! << NEW_LINE
  17. end
  18. 1 private
  19. 1 attr_reader :template, :parser
  20. 1 def concat attributes
  21. 9 parser.call(template, attributes).each.with_object(+"") do |(key, value), line|
  22. 44 line << key.to_s << "=" << escape(value) << " "
  23. end
  24. end
  25. end
  26. end
  27. end

lib/cogger/formatters/sanitizers/escape.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Formatters
  4. 1 module Sanitizers
  5. # Sanitizes value as fully quoted string for emojis, spaces, and control characters.
  6. 1 class Escape
  7. 1 PATTERN = /
  8. \A # Search string start.
  9. .* # Match zero or more characters.
  10. ( # Conditional start.
  11. (?!\p{Number}) # Look ahead and ignore unicode numbers.
  12. \p{Emoji} # Match unicode emoji only.
  13. | # Or.
  14. [[:space:]] # Match spaces, tabs, and new lines.
  15. | # Or.
  16. [[:cntrl:]] # Match control characters.
  17. ) # Conditional end.
  18. .* # Match zero or more characters.
  19. \z # Search string end.
  20. /xu
  21. 1 def initialize pattern: PATTERN
  22. 10 @pattern = pattern
  23. end
  24. 1 def call value
  25. 53 else: 2 then: 51 return dump value unless value.is_a? Array
  26. 7 value.reduce(+"") { |text, item| text << dump(item) << ", " }
  27. 2 .then { |text| %([#{text.delete_suffix ", "}]).dump }
  28. end
  29. 1 private
  30. 1 attr_reader :pattern
  31. 1 def dump(value) = value.to_s.gsub(pattern, &:dump)
  32. end
  33. end
  34. end
  35. end

lib/cogger/formatters/sanitizers/filter.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Formatters
  4. 1 module Sanitizers
  5. # Sanitizes/removes sensitive values.
  6. 1 Filter = lambda do |attributes, filters: Cogger.filters|
  7. 127 then: 3 else: 35 filters.each { |key| attributes[key] = "[FILTERED]" if attributes.key? key }
  8. 89 attributes
  9. end
  10. end
  11. end
  12. end

lib/cogger/formatters/sanitizers/format_time.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "date"
  3. 1 module Cogger
  4. 1 module Formatters
  5. 1 module Sanitizers
  6. # Sanitizes/formats date/time value.
  7. 1 FormatTime = lambda do |value, format: Cogger::DATETIME_FORMAT|
  8. 428 else: 89 then: 339 return value unless value.is_a?(::Time) || value.is_a?(Date) || value.is_a?(DateTime)
  9. 89 value.strftime format
  10. end
  11. end
  12. end
  13. end

lib/cogger/formatters/simple.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Formatters
  4. # Formats simple templates that require minimal processing.
  5. 1 class Simple < Abstract
  6. 1 TEMPLATE = "[%<id>s] %<message>s"
  7. 1 def initialize template = TEMPLATE
  8. 11 super()
  9. 11 @template = template
  10. end
  11. 1 def call(*input)
  12. 10 *, entry = input
  13. 10 attributes = sanitize entry, :tagged
  14. 10 format(template, attributes).tap(&:strip!) << NEW_LINE
  15. end
  16. 1 private
  17. 1 attr_reader :template, :processor
  18. end
  19. end
  20. end

lib/cogger/formatters/transformers/color.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Formatters
  4. 1 module Transformers
  5. # Transforms target into colorized string.
  6. 1 class Color
  7. 1 def initialize emoji: Emoji::KEY, key_transformer: Key, registry: Cogger
  8. 6 @emoji = emoji
  9. 6 @key_transformer = key_transformer
  10. 6 @registry = registry
  11. end
  12. 1 def call target, directive, level
  13. 145 then: 2 else: 143 return target if !target.is_a?(String) || target == emoji
  14. 143 key = key_transformer.call directive, level
  15. 143 then: 142 else: 1 return client.encode target, key if aliases.key?(key) || defaults.key?(key)
  16. 1 target
  17. end
  18. 1 private
  19. 1 attr_reader :emoji, :key_transformer, :registry
  20. 1 def aliases = registry.aliases
  21. 1 def defaults = client.defaults
  22. 1 def client = registry.color
  23. end
  24. end
  25. end
  26. end

lib/cogger/formatters/transformers/emoji.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Formatters
  4. 1 module Transformers
  5. # Transforms target into emoji.
  6. 1 class Emoji
  7. 1 KEY = "emoji"
  8. 1 def initialize key = KEY, key_transformer: Key, registry: Cogger
  9. 5 @key = key
  10. 5 @key_transformer = key_transformer
  11. 5 @registry = registry
  12. end
  13. 1 def call target, directive, level
  14. 48 else: 47 then: 1 return target unless target == key
  15. 47 key = key_transformer.call directive, level
  16. 47 then: 46 else: 1 registry.aliases.key?(key) ? registry.get_emoji(key) : target
  17. end
  18. 1 private
  19. 1 attr_reader :key, :key_transformer, :registry
  20. end
  21. end
  22. end
  23. end

lib/cogger/formatters/transformers/key.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Formatters
  4. 1 module Transformers
  5. # Transforms directive, based on log level, into a key for color or emoji lookup.
  6. 194 then: 160 else: 33 Key = -> directive, level { (directive == "dynamic" ? level.downcase : directive).to_sym }
  7. end
  8. end
  9. end

lib/cogger/hub.rb

100.0% lines covered

100.0% branches covered

51 relevant lines. 51 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "forwardable"
  3. 1 require "logger"
  4. 1 require "refinements/hash"
  5. 1 module Cogger
  6. # Loads configuration and simultaneously sends messages to multiple streams.
  7. # :reek:TooManyMethods
  8. 1 class Hub
  9. 1 extend Forwardable
  10. 1 using Refinements::Hash
  11. 1 using Refines::Logger
  12. 1 delegate %i[
  13. close
  14. reopen
  15. debug!
  16. debug?
  17. info!
  18. info?
  19. warn!
  20. warn?
  21. error!
  22. error?
  23. fatal!
  24. fatal?
  25. formatter
  26. formatter=
  27. level
  28. level=
  29. ] => :primary
  30. 1 delegate %i[id io tags mode age size suffix] => :configuration
  31. 1 def initialize(registry: Cogger, model: Configuration, **attributes)
  32. 57 @registry = registry
  33. 57 @configuration = model[**find_formatter(attributes)]
  34. 57 @primary = configuration.to_logger
  35. 57 @streams = [@primary]
  36. end
  37. 1 def add_stream **attributes
  38. 1 attributes[:id] = configuration.id
  39. 1 streams.append configuration.with(**find_formatter(attributes)).to_logger
  40. 1 self
  41. end
  42. 1 def debug(message = nil, **, &) = log(__method__, message, **, &)
  43. 1 def info(message = nil, **, &) = log(__method__, message, **, &)
  44. 1 def warn(message = nil, **, &) = log(__method__, message, **, &)
  45. 1 def error(message = nil, **, &) = log(__method__, message, **, &)
  46. 1 def fatal(message = nil, **, &) = log(__method__, message, **, &)
  47. 1 def any(message = nil, **, &) = log(__method__, message, **, &)
  48. 1 def abort(message = nil, **payload, &block)
  49. 4 then: 3 else: 1 error(message, **payload, &block) if message || !payload.empty? || block
  50. 4 exit false
  51. end
  52. 1 def add(level, message = nil, **, &)
  53. 3 log(Logger::SEV_LABEL.fetch(level, "ANY").downcase, message, **, &)
  54. end
  55. 1 alias unknown any
  56. 1 def reread = primary.reread
  57. 1 def inspect
  58. 7 %(#<#{self.class} #{configuration.inspect.delete_prefix! "#<Cogger::Configuration "})
  59. end
  60. 1 private
  61. 1 attr_reader :registry, :configuration, :primary, :streams
  62. # :reek:FeatureEnvy
  63. 1 def find_formatter attributes
  64. 58 attributes.transform_value! :formatter do |value|
  65. 16 else: 8 then: 8 next value unless value.is_a?(Symbol) || value.is_a?(String)
  66. 8 formatter, template = registry.get_formatter value
  67. 8 then: 1 else: 7 template ? formatter.new(template) : formatter.new
  68. end
  69. end
  70. 1 def log(level, message = nil, **, &)
  71. 46 dispatch(level, message, **, &)
  72. rescue StandardError => error
  73. 7 crash message, error
  74. end
  75. # rubocop:todo Metrics/MethodLength
  76. 1 def dispatch(level, message, **payload, &)
  77. 46 entry = configuration.entry.for(
  78. message,
  79. id: configuration.id,
  80. level:,
  81. tags: configuration.entag(payload.delete(:tags)),
  82. datetime_format: configuration.datetime_format,
  83. **payload,
  84. &
  85. )
  86. 138 configuration.mutex.synchronize { streams.each { |logger| logger.public_send level, entry } }
  87. 39 true
  88. end
  89. # rubocop:enable Metrics/MethodLength
  90. 1 def crash message, error
  91. 7 configuration.with(id: :cogger, io: $stdout, formatter: Formatters::Crash.new)
  92. .to_logger
  93. .fatal configuration.entry.for_crash(message, error, id: configuration.id)
  94. 7 true
  95. end
  96. end
  97. end

lib/cogger/level.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "logger"
  3. 1 require "refinements/array"
  4. # Loads log level from environment.
  5. 1 module Cogger
  6. 1 using Refinements::Array
  7. 1 Level = lambda do |logger = Logger, environment: ENV, allowed: LEVELS|
  8. 36 value = String environment.fetch("LOG_LEVEL", "INFO")
  9. 36 then: 35 else: 1 return logger.const_get value.upcase if allowed.include? value.downcase
  10. 1 fail ArgumentError, %(Invalid log level: #{value.inspect}. Use: #{allowed.to_usage "or"}.)
  11. end
  12. end

lib/cogger/program.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "pathname"
  3. # Computes default program name based on current file name.
  4. 1 module Cogger
  5. 1 Program = lambda do |name = $PROGRAM_NAME|
  6. 250 Pathname(name).then { |path| path.basename(path.extname).to_s }
  7. end
  8. end

lib/cogger/rack/logger.rb

100.0% lines covered

100.0% branches covered

22 relevant lines. 22 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 module Cogger
  4. 1 module Rack
  5. # Middlware for enriched logging based on the incoming request.
  6. 1 class Logger
  7. DEFAULTS = {
  8. 1 logger: Cogger.new(formatter: :json),
  9. timer: Cogger::Time::Span.new,
  10. key_map: {
  11. verb: "REQUEST_METHOD",
  12. ip: "REMOTE_ADDR",
  13. path: "PATH_INFO",
  14. params: "QUERY_STRING",
  15. length: "CONTENT_LENGTH"
  16. }
  17. }.freeze
  18. 1 def initialize application, options = Core::EMPTY_HASH, defaults: DEFAULTS
  19. 4 configuration = defaults.merge options
  20. 4 @application = application
  21. 4 @logger = configuration.fetch :logger
  22. 4 @timer = configuration.fetch :timer
  23. 4 @key_map = configuration.fetch :key_map
  24. end
  25. 1 def call environment
  26. 4 request = ::Rack::Request.new environment
  27. 8 (status, headers, body), duration, unit = timer.call { application.call environment }
  28. 4 logger.info tags: [tags_for(request), {status:, duration:, unit:}]
  29. 4 [status, headers, body]
  30. end
  31. 1 private
  32. 1 attr_reader :application, :logger, :timer, :key_map
  33. 1 def tags_for request
  34. 4 key_map.each_key.with_object({}) do |tag, collection|
  35. 20 key = key_map.fetch tag, tag
  36. 20 collection[String(tag).downcase] = request.get_header key
  37. end
  38. end
  39. end
  40. end
  41. end

lib/cogger/refines/log_device.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
3 total branches, 3 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "logger"
  3. 1 require "refinements/string_io"
  4. 1 module Cogger
  5. 1 module Refines
  6. # Provides additional enhancements to a log device.
  7. 1 module LogDevice
  8. 1 using Refinements::StringIO
  9. 1 refine ::Logger::LogDevice do
  10. 1 def reread
  11. 11 when: 2 case dev
  12. 2 when: 7 when ::File then dev.class.new(dev).read
  13. 7 else: 2 when ::StringIO then dev.reread
  14. 2 else ""
  15. end
  16. end
  17. end
  18. end
  19. end
  20. end

lib/cogger/refines/logger.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Refines
  4. # Provides additional enhancements to a logger.
  5. 1 module Logger
  6. 1 using LogDevice
  7. 1 refine ::Logger do
  8. 1 def reread = @logdev.reread
  9. 1 alias_method :any, :unknown
  10. end
  11. end
  12. end
  13. end

lib/cogger/registry.rb

100.0% lines covered

100.0% branches covered

41 relevant lines. 41 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/hash"
  3. 1 require "tone"
  4. 1 module Cogger
  5. # Provides a global regsitry for global configuration.
  6. 1 module Registry
  7. 1 using Refinements::Hash
  8. 1 def self.extended descendant
  9. 38 descendant.add_alias(:debug, :white)
  10. .add_alias(:info, :green)
  11. .add_alias(:warn, :yellow)
  12. .add_alias(:error, :red)
  13. .add_alias(:fatal, :bold, :white, :on_red)
  14. .add_alias(:any, :dim, :bright_white)
  15. .add_emojis(
  16. debug: "๐Ÿ”Ž",
  17. info: "๐ŸŸข",
  18. warn: "โš ๏ธ",
  19. error: "๐Ÿ›‘",
  20. fatal: "๐Ÿ”ฅ",
  21. any: "โšซ๏ธ"
  22. )
  23. .add_formatter(:color, Cogger::Formatters::Color)
  24. .add_formatter(
  25. :detail,
  26. Cogger::Formatters::Simple,
  27. "[%<id>s] [%<level>s] [%<at>s] %<message>s"
  28. )
  29. .add_formatter(:emoji, Cogger::Formatters::Emoji)
  30. .add_formatter(:json, Cogger::Formatters::JSON)
  31. .add_formatter(:property, Cogger::Formatters::Property)
  32. .add_formatter(:simple, Cogger::Formatters::Simple)
  33. .add_formatter :rack,
  34. Cogger::Formatters::Simple,
  35. "[%<id>s] [%<level>s] [%<at>s] %<verb>s %<status>s " \
  36. "%<duration>s %<ip>s %<path>s %<length>s %<params>s"
  37. end
  38. 1 def add_alias(key, *styles)
  39. 230 color.add_alias(key, *styles)
  40. 230 self
  41. end
  42. 1 def aliases = color.aliases
  43. 1 def add_emoji key, value
  44. 4 warn "`#{self.class}##{__method__}` is deprecated, use `#add_emojis` instead.",
  45. category: :deprecated
  46. 4 emojis[key.to_sym] = value
  47. 4 self
  48. end
  49. 1 def add_emojis(**attributes)
  50. 44 emojis.merge! attributes.symbolize_keys!
  51. 44 self
  52. end
  53. 1 def get_emoji key
  54. 52 emojis.fetch(key.to_sym) { fail KeyError, "Unregistered emoji: #{key}." }
  55. end
  56. 1 def emojis = @emojis ||= {}
  57. 1 def add_filter key
  58. 4 warn "`#{self.class}##{__method__}` is deprecated, use `#add_filters` instead.",
  59. category: :deprecated
  60. 4 filters.add key.to_sym
  61. 4 self
  62. end
  63. 1 def add_filters(*keys)
  64. 5 filters.merge(keys.map(&:to_sym))
  65. 5 self
  66. end
  67. 1 def filters = @filters ||= Set.new
  68. 1 def add_formatter key, formatter, template = nil
  69. 274 formatters[key.to_sym] = [formatter, template || formatter::TEMPLATE]
  70. 273 self
  71. rescue NameError
  72. 1 raise NameError, "#{formatter}::TEMPLATE must be defined with a default template string."
  73. end
  74. 1 def get_formatter key
  75. 16 formatters.fetch(key.to_sym) { fail KeyError, "Unregistered formatter: #{key}." }
  76. end
  77. 1 def formatters = @formatters ||= {}
  78. 1 def templates
  79. 1 formatters.each.with_object({}) do |(key, (_formatter, template)), collection|
  80. 7 collection[key] = template
  81. end
  82. end
  83. 1 def color = @color ||= Tone.new
  84. 1 def defaults = {emojis:, aliases:, formatters:, filters:, color:}
  85. end
  86. end

lib/cogger/tag.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
8 total branches, 8 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "refinements/hash"
  4. 1 module Cogger
  5. # Models a tag which may consist of an array and/or hash.
  6. 1 Tag = Data.define :singles, :pairs do
  7. 1 using Refinements::Hash
  8. 1 def self.for(*bag)
  9. 93 bag.each.with_object new do |item, tag|
  10. 34 then: 3 else: 31 value = item.is_a?(Proc) ? item.call : item
  11. 34 then: 17 else: 17 value.is_a?(Hash) ? tag.pairs.merge!(value) : tag.singles.append(value)
  12. end
  13. end
  14. 1 def initialize singles: [], pairs: {}
  15. 116 super
  16. end
  17. 1 def empty? = singles.empty? && pairs.empty?
  18. 12 then: 1 else: 10 def to_h = empty? ? Core::EMPTY_HASH : {tags: singles.to_a, **pairs}.tap(&:compress!)
  19. 6 then: 1 else: 4 def to_s = empty? ? Core::EMPTY_STRING : "#{format_singles} #{format_pairs}".tap(&:strip!)
  20. 1 private
  21. 1 def format_singles
  22. 11 singles.map { |value| "[#{value}]" }
  23. .join " "
  24. end
  25. 1 def format_pairs
  26. 8 pairs.map { |key, value| "[#{key}=#{value}]" }
  27. .join(" ")
  28. end
  29. end
  30. end

lib/cogger/time/clock.rb

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Time
  4. # An adapter for acquiring current time.
  5. 12 Clock = -> id = Process::CLOCK_MONOTONIC, unit: :nanosecond { Process.clock_gettime id, unit }
  6. end
  7. end

lib/cogger/time/range.rb

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Time
  4. RANGE = {
  5. 1 nanoseconds: ...1_000,
  6. microseconds: 1_000...1_000_000,
  7. milliseconds: 1_000_000...1_000_000_000,
  8. seconds: 1_000_000_000...60_000_000_000,
  9. minutes: 60_000_000_000...
  10. }.freeze
  11. end
  12. end

lib/cogger/time/span.rb

100.0% lines covered

100.0% branches covered

32 relevant lines. 32 lines covered and 0 lines missed.
5 total branches, 5 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Time
  4. # Measures duration of a process with nanosecond precision.
  5. 1 class Span
  6. 1 def initialize clock = Clock, unit: Unit, range: RANGE
  7. 6 @clock = clock
  8. 6 @unit = unit
  9. 6 @range = range
  10. end
  11. 1 def call
  12. 9 start = current
  13. 9 result = yield
  14. 9 span = current - start
  15. 9 [result, duration(span), unit.call(span)]
  16. end
  17. 1 private
  18. 1 attr_reader :clock, :unit, :range
  19. 1 def duration value
  20. 9 when: 1 case value
  21. 1 when: 5 when nanoseconds then value
  22. 5 when: 1 when microseconds then value / microseconds.min
  23. 1 when: 1 when milliseconds then value / milliseconds.min
  24. 1 else: 1 when seconds then value / seconds.min
  25. 1 else value / minutes.min
  26. end
  27. end
  28. 1 def current = clock.call
  29. 1 def nanoseconds
  30. 9 @nanoseconds ||= range.fetch __method__
  31. end
  32. 1 def microseconds
  33. 13 @microseconds ||= range.fetch __method__
  34. end
  35. 1 def milliseconds
  36. 4 @milliseconds ||= range.fetch __method__
  37. end
  38. 1 def seconds
  39. 3 @seconds ||= range.fetch __method__
  40. end
  41. 1 def minutes
  42. 1 @minutes ||= range.fetch __method__
  43. end
  44. end
  45. end
  46. end

lib/cogger/time/unit.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
5 total branches, 5 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Cogger
  3. 1 module Time
  4. # Provides unit of measure for duration.
  5. 1 Unit = lambda do |duration, range: RANGE|
  6. 14 when: 2 case duration
  7. 2 when: 6 when range.fetch(:nanoseconds) then "ns"
  8. 6 when: 2 when range.fetch(:microseconds) then "ยตs"
  9. 2 when: 2 when range.fetch(:milliseconds) then "ms"
  10. 2 else: 2 when range.fetch(:seconds) then "s"
  11. 2 else "m"
  12. end
  13. end
  14. end
  15. end