cogger 0.6.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
data/cogger.gemspec CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "cogger"
5
- spec.version = "0.6.0"
5
+ spec.version = "0.7.1"
6
6
  spec.authors = ["Brooke Kuhlmann"]
7
7
  spec.email = ["brooke@alchemists.io"]
8
8
  spec.homepage = "https://alchemists.io/projects/cogger"
9
- spec.summary = "Decorates native logging with colorized output."
9
+ spec.summary = "A customizable logger."
10
10
  spec.license = "Hippocratic-2.1"
11
11
 
12
12
  spec.metadata = {
@@ -23,8 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.cert_chain = [Gem.default_cert_path]
24
24
 
25
25
  spec.required_ruby_version = "~> 3.2"
26
- spec.add_dependency "pastel", "~> 0.8"
27
- spec.add_dependency "refinements", "~> 10.0"
26
+ spec.add_dependency "tone", "~> 0.1"
28
27
  spec.add_dependency "zeitwerk", "~> 2.6"
29
28
 
30
29
  spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
data/lib/cogger/client.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "forwardable"
4
4
  require "logger"
5
5
  require "refinements/loggers"
6
+ require "tone"
6
7
 
7
8
  module Cogger
8
9
  # Provides the primary client for colorized logging.
@@ -13,7 +14,7 @@ module Cogger
13
14
 
14
15
  delegate %i[formatter level progname debug info warn error fatal unknown] => :logger
15
16
 
16
- def initialize logger = Logger.new($stdout), color: Color.new, **attributes
17
+ def initialize logger = Logger.new($stdout), color: Cogger.color, **attributes
17
18
  @logger = logger
18
19
  @color = color
19
20
  @attributes = attributes
@@ -42,7 +43,7 @@ module Cogger
42
43
  def default_level = logger.class.const_get ENV.fetch("LOG_LEVEL", "INFO")
43
44
 
44
45
  def default_formatter
45
- -> severity, _at, _name, message { "#{color.public_send severity.downcase, message}\n" }
46
+ -> severity, _at, _name, message { "#{color[message, severity.downcase]}\n" }
46
47
  end
47
48
  end
48
49
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Cogger
6
+ # Defines the default configuration for all pipes.
7
+ Configuration = Data.define(
8
+ :id,
9
+ :io,
10
+ :level,
11
+ :formatter,
12
+ :mode,
13
+ :age,
14
+ :size,
15
+ :suffix,
16
+ :logger
17
+ ) do
18
+ def initialize id: Program.call,
19
+ io: $stdout,
20
+ level: Logger.const_get(ENV.fetch("LOG_LEVEL", "INFO")),
21
+ formatter: Formatters::Color.new,
22
+ mode: false,
23
+ age: 0,
24
+ size: 1_048_576,
25
+ suffix: "%Y-%m-%d",
26
+ logger: Logger
27
+ super
28
+ end
29
+
30
+ def to_logger
31
+ logger.new io,
32
+ age,
33
+ size,
34
+ progname: id,
35
+ level:,
36
+ formatter:,
37
+ binmode: mode,
38
+ shift_period_suffix: suffix
39
+ end
40
+
41
+ def inspect
42
+ "#<#{self.class} @id=#{id}, @io=#{io.class}, @level=#{level}, " \
43
+ "@formatter=#{formatter.class}, " \
44
+ "@mode=#{mode}, @age=#{age}, @size=#{size}, @suffix=#{suffix.inspect}, " \
45
+ "@logger=#{logger}>"
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cogger
4
+ module Formatters
5
+ # Formats by color.
6
+ class Color
7
+ TEMPLATE = "%<message:dynamic>s"
8
+
9
+ def initialize template = TEMPLATE, processor: Processors::Color.new
10
+ @template = template
11
+ @processor = processor
12
+ end
13
+
14
+ def call(*entry)
15
+ updated_template, attributes = processor.call(template, *entry)
16
+ "#{format(updated_template, **attributes)}\n"
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :template, :processor
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cogger
4
+ module Formatters
5
+ # Formats fatal crashes.
6
+ class Crash
7
+ TEMPLATE = <<~CONTENT
8
+ <dynamic>[%<id>s] [%<severity>s] [%<at>s] Crash!
9
+ %<message>s
10
+ %<error_message>s (%<error_class>s)
11
+ %<backtrace>s</dynamic>
12
+ CONTENT
13
+
14
+ def initialize template = TEMPLATE, processor: Processors::Color.new
15
+ @template = template
16
+ @processor = processor
17
+ end
18
+
19
+ def call(*entry)
20
+ updated_template, attributes = processor.call(template, *entry)
21
+ attributes[:backtrace] = %( #{attributes[:backtrace].join "\n "})
22
+ "#{format(updated_template, **attributes)}\n"
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :template, :processor
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Cogger
6
+ module Formatters
7
+ # Formats as JSON output.
8
+ class JSON
9
+ TEMPLATE = "%<id>s %<severity>s %<at>s %<message>s"
10
+
11
+ def initialize template = TEMPLATE,
12
+ parser: Parsers::Individual.new,
13
+ sanitizer: Kit::Sanitizer.new
14
+ @template = template
15
+ @parser = parser
16
+ @sanitizer = sanitizer
17
+ end
18
+
19
+ def call(*entry)
20
+ positions = parser.call(template).last.keys
21
+ attributes = sanitizer.call(*entry).tap(&:compact!)
22
+ "#{attributes.slice(*positions).merge!(attributes.except(*positions)).to_json}\n"
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :template, :parser, :sanitizer
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cogger
4
+ module Formatters
5
+ module Kit
6
+ # Transform color based on dynamic (severity) or standard color preference.
7
+ Colorizer = lambda do |value, attributes|
8
+ value == "dynamic" ? attributes[:severity].downcase : value
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cogger
4
+ module Formatters
5
+ module Kit
6
+ # Transforms a positional log entry into a hash entry for template parsing and formatting.
7
+ class Sanitizer
8
+ def initialize filters: Cogger.filters
9
+ @filters = filters
10
+ end
11
+
12
+ # :reek:FeatureEnvy
13
+ def call(*entry)
14
+ severity, at, id, message = entry
15
+
16
+ attributes = if message.is_a? Hash
17
+ {id:, severity:, at:, message: nil, **message.except(:id, :severity, :at)}
18
+ else
19
+ {id:, severity:, at:, message:}
20
+ end
21
+
22
+ filter attributes
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :filters
28
+
29
+ # :reek:FeatureEnvy
30
+ def filter attributes
31
+ filters.each { |key| attributes[key] = "[FILTERED]" if attributes.key? key }
32
+ attributes
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cogger
4
+ module Formatters
5
+ module Parsers
6
+ # Dynamically extracts the universal or individual template attributes for log entry parsing.
7
+ class Dynamic
8
+ # Order matters.
9
+ DELEGATES = [Universal.new, Individual.new].freeze
10
+
11
+ def initialize delegates: DELEGATES
12
+ @delegates = delegates
13
+ end
14
+
15
+ def call(template) = parse(template) || template
16
+
17
+ private
18
+
19
+ attr_reader :delegates
20
+
21
+ def parse template
22
+ delegates.find do |delegate|
23
+ result = delegate.call template
24
+ break result unless result == template
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cogger
4
+ module Formatters
5
+ module Parsers
6
+ # Sanitizes and extracts individual directives from a template.
7
+ class Individual
8
+ # rubocop:todo Lint/MixedRegexpCaptureTypes
9
+ PATTERN = /
10
+ % # Strict reference syntax.
11
+ (?<flag>[\s#+-0*])? # Optional flag.
12
+ (?<width>\d+)? # Optional width.
13
+ \.? # Optional precision delimiter.
14
+ (?<precision>\d+)? # Optional precision value.
15
+ < # Reference start.
16
+ ( # Conditional start.
17
+ (?<key>\w+) # Key.
18
+ : # Directive delimiter.
19
+ (?<directive>\w+) # Value.
20
+ | # Conditional.
21
+ (?<key>\w+) # Key.
22
+ ) # Conditional end.
23
+ > # Reference end.
24
+ (?<specifier>[ABEGXabcdefgiopsux]) # Specifier.
25
+ /mx
26
+ # rubocop:enable Lint/MixedRegexpCaptureTypes
27
+
28
+ def initialize pattern: PATTERN
29
+ @pattern = pattern
30
+ end
31
+
32
+ def call template
33
+ attributes = {}
34
+
35
+ return [template, attributes] unless template.match? pattern
36
+
37
+ template = sanitize_and_extract template, attributes
38
+ [template, attributes]
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :pattern
44
+
45
+ # :reek:FeatureEnvy
46
+ # :reek:TooManyStatements
47
+ def sanitize_and_extract template, attributes
48
+ template.dup.gsub pattern do
49
+ captures = Regexp.last_match.named_captures
50
+ attributes[captures["key"].to_sym] = captures["directive"]
51
+
52
+ captures.reduce(+"%") do |body, (key, value)|
53
+ next body if key == "directive"
54
+
55
+ body.concat key == "key" ? "<#{value}>" : value.to_s
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cogger
4
+ module Formatters
5
+ module Parsers
6
+ # Sanitizes and extracts the universal directive a template.
7
+ class Universal
8
+ # rubocop:todo Lint/MixedRegexpCaptureTypes
9
+ PATTERN = %r(
10
+ ( # Conditional start.
11
+ \A # Search start.
12
+ < # Tag start.
13
+ (?<directive>\w+) # Directive.
14
+ > # Tag end.
15
+ | # Conditional pipe.
16
+ < # Tag start.
17
+ / # Tag close.
18
+ (?<directive>\w+) # Directive.
19
+ > # Tag end.
20
+ \Z # Search end.
21
+ ) # Conditional end.
22
+ )mx
23
+ # rubocop:enable Lint/MixedRegexpCaptureTypes
24
+
25
+ KEY = "directive"
26
+
27
+ def initialize pattern: PATTERN, key: KEY
28
+ @pattern = pattern
29
+ @key = key
30
+ end
31
+
32
+ def call template
33
+ return template unless template.match? pattern
34
+
35
+ [template.gsub(pattern, ""), template.match(pattern)[key]]
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :pattern, :key
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tone"
4
+
5
+ module Cogger
6
+ module Formatters
7
+ module Processors
8
+ # Processes emojis and colors.
9
+ class Color
10
+ def initialize parser: Parsers::Dynamic.new,
11
+ kit: {sanitizer: Kit::Sanitizer.new, colorizer: Kit::Colorizer},
12
+ registry: Cogger
13
+ @parser = parser
14
+ @kit = kit
15
+ @registry = registry
16
+ end
17
+
18
+ def call(template, *entry)
19
+ attributes = sanitizer.call(*entry)
20
+
21
+ case parser.call template
22
+ in [String => body, String => style] then universal body, style, **attributes
23
+ in [String => body, Hash => styles] then individual body, attributes, styles
24
+ # :nocov:
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :parser, :kit, :registry
31
+
32
+ def universal body, style, **attributes
33
+ [registry.color[body, colorizer.call(style, attributes)], attributes]
34
+ end
35
+
36
+ def individual body, attributes, styles
37
+ attributes = attributes.each.with_object({}) do |(key, value), collection|
38
+ collection[key] = registry.color[value, colorizer.call(styles[key], attributes)]
39
+ end
40
+
41
+ emojify attributes, styles
42
+ [body, attributes]
43
+ end
44
+
45
+ def emojify attributes, styles
46
+ style = styles[:emoji]
47
+
48
+ return unless style
49
+
50
+ attributes[:emoji] = registry.get_emoji colorizer.call(style, attributes)
51
+ end
52
+
53
+ def sanitizer = kit.fetch :sanitizer
54
+
55
+ def colorizer = kit.fetch :colorizer
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cogger
4
+ module Formatters
5
+ # Formats simple templates that require no additional processing.
6
+ class Simple
7
+ TEMPLATE = "%<message>s"
8
+
9
+ def initialize template = TEMPLATE, sanitizer: Kit::Sanitizer.new
10
+ @template = template
11
+ @sanitizer = sanitizer
12
+ end
13
+
14
+ def call(*entry) = "#{format template, sanitizer.call(*entry)}\n"
15
+
16
+ private
17
+
18
+ attr_reader :template, :sanitizer
19
+ end
20
+ end
21
+ end
data/lib/cogger/hub.rb ADDED
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Cogger
6
+ # Loads configuration and simultaneously sends messages to multiple streams.
7
+ class Hub
8
+ def initialize(registry: Cogger, model: Configuration.new, **attributes)
9
+ @registry = registry
10
+ @configuration = model.with(**transform(attributes))
11
+ @mutex = Mutex.new
12
+ @streams = [configuration.to_logger]
13
+ end
14
+
15
+ def add_stream **attributes
16
+ attributes[:id] = configuration.id
17
+ streams.append configuration.with(**transform(attributes)).to_logger
18
+ self
19
+ end
20
+
21
+ def debug(...) = log(__method__, ...)
22
+
23
+ def info(...) = log(__method__, ...)
24
+
25
+ def warn(...) = log(__method__, ...)
26
+
27
+ def error(...) = log(__method__, ...)
28
+
29
+ def fatal(...) = log(__method__, ...)
30
+
31
+ def unknown(...) = log(__method__, ...)
32
+
33
+ alias any unknown
34
+
35
+ def inspect
36
+ %(#<#{self.class} #{configuration.inspect.delete_prefix! "#<Cogger::Configuration "})
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :registry, :configuration, :mutex, :streams
42
+
43
+ # :reek:FeatureEnvy
44
+ # :reek:TooManyStatements
45
+ def transform attributes
46
+ value = attributes[:formatter]
47
+
48
+ return attributes unless value.is_a?(Symbol) || value.is_a?(String)
49
+
50
+ formatter, template = registry.get_formatter value
51
+ attributes[:formatter] = template ? formatter.new(template) : formatter.new
52
+ attributes
53
+ end
54
+
55
+ # :reek:TooManyStatements
56
+ def log(severity, message = nil, &)
57
+ mutex.synchronize { streams.each { |logger| logger.public_send(severity, message, &) } }
58
+ true
59
+ rescue StandardError => error
60
+ configuration.with(id: "Cogger", io: $stdout, formatter: Formatters::Crash.new)
61
+ .to_logger
62
+ .fatal message:,
63
+ error_message: error.message,
64
+ error_class: error.class,
65
+ backtrace: error.backtrace
66
+ true
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ # Provides a function for computing the default program name based on current file.
6
+ module Cogger
7
+ Program = lambda do |name = $PROGRAM_NAME|
8
+ Pathname(name).then { |path| path.basename(path.extname).to_s }
9
+ end
10
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tone"
4
+
5
+ module Cogger
6
+ # Provides a global regsitry for global configuration.
7
+ module Registry
8
+ def self.extended target
9
+ target.add_alias(:debug, :white)
10
+ .add_alias(:info, :green)
11
+ .add_alias(:warn, :yellow)
12
+ .add_alias(:error, :red)
13
+ .add_alias(:fatal, *%i[bold white on_red])
14
+ .add_alias(:unknown, *%i[bold white])
15
+ .add_alias(:any, *%i[bold white])
16
+ .add_emoji(:debug, "🔎")
17
+ .add_emoji(:info, "🟢")
18
+ .add_emoji(:warn, "⚠️ ")
19
+ .add_emoji(:error, "🛑")
20
+ .add_emoji(:fatal, "🔥")
21
+ .add_filter(:_csrf)
22
+ .add_filter(:password)
23
+ .add_filter(:password_confirmation)
24
+ .add_formatter(:color, Cogger::Formatters::Color)
25
+ .add_formatter(
26
+ :detail,
27
+ Cogger::Formatters::Simple,
28
+ "[%<id>s] [%<severity>s] [%<at>s] %<message>s"
29
+ )
30
+ .add_formatter(
31
+ :emoji,
32
+ Cogger::Formatters::Color,
33
+ "%<emoji:dynamic>s %<message:dynamic>s"
34
+ )
35
+ .add_formatter(:json, Cogger::Formatters::JSON)
36
+ .add_formatter(:simple, Cogger::Formatters::Simple)
37
+ .add_formatter :rack,
38
+ Cogger::Formatters::Simple,
39
+ "[%<id>s] [%<severity>s] [%<at>s] %<verb>s %<status>s %<duration>s " \
40
+ "%<ip>s %<path>s %<length>s %<params>s"
41
+ end
42
+
43
+ def add_alias(key, *styles)
44
+ color.add_alias(key, *styles)
45
+ self
46
+ end
47
+
48
+ def aliases = color.aliases
49
+
50
+ def add_emoji key, value
51
+ emojis[key.to_sym] = value
52
+ self
53
+ end
54
+
55
+ def get_emoji(key) = emojis[key.to_sym]
56
+
57
+ def emojis = @emojis ||= {}
58
+
59
+ def add_filter key
60
+ filters.add key.to_sym
61
+ self
62
+ end
63
+
64
+ def filters = @filters ||= Set.new
65
+
66
+ def add_formatter key, formatter, template = nil
67
+ formatters[key.to_sym] = [formatter, template]
68
+ self
69
+ end
70
+
71
+ def get_formatter(key) = formatters[key.to_sym]
72
+
73
+ def formatters = @formatters ||= {}
74
+
75
+ def color = @color ||= Tone.new
76
+
77
+ def defaults = {emojis:, aliases:, formatters:, filters:, color:}
78
+ end
79
+ end
data/lib/cogger.rb CHANGED
@@ -2,9 +2,19 @@
2
2
 
3
3
  require "zeitwerk"
4
4
 
5
- Zeitwerk::Loader.for_gem.setup
5
+ Zeitwerk::Loader.for_gem.then do |loader|
6
+ loader.inflector.inflect "json" => "JSON"
7
+ loader.setup
8
+ end
6
9
 
7
10
  # Main namespace.
8
11
  module Cogger
9
- def self.init(...) = Client.new(...)
12
+ extend Registry
13
+
14
+ def self.init(...)
15
+ warn "#{self}##{__method__} is deprecated, use `.new` instead.", category: :deprecated
16
+ Client.new(...)
17
+ end
18
+
19
+ def self.new(...) = Hub.new(...)
10
20
  end
data.tar.gz.sig CHANGED
Binary file