cogger 0.5.1 → 0.7.0

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.
@@ -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)
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.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,61 @@
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
+ return template unless template.match? pattern
34
+
35
+ attributes = {}
36
+ template = sanitize_and_extract template, attributes
37
+ [template, attributes]
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :pattern
43
+
44
+ # :reek:FeatureEnvy
45
+ # :reek:TooManyStatements
46
+ def sanitize_and_extract template, attributes
47
+ template.dup.gsub pattern do
48
+ captures = Regexp.last_match.named_captures
49
+ attributes[captures["key"].to_sym] = captures["directive"]
50
+
51
+ captures.reduce(+"%") do |body, (key, value)|
52
+ next body if key == "directive"
53
+
54
+ body.concat key == "key" ? "<#{value}>" : value.to_s
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ 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
+ else [template, {}]
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: emojis.dup, formatters: formatters.dup}
78
+ end
79
+ end
data/lib/cogger.rb CHANGED
@@ -2,8 +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
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(...)
9
20
  end
data.tar.gz.sig CHANGED
Binary file