-
# frozen_string_literal: true
-
-
1
require "zeitwerk"
-
-
1
Zeitwerk::Loader.new.then do |loader|
-
1
loader.inflector.inflect "json" => "JSON", "range" => "RANGE"
-
1
loader.tag = File.basename __FILE__, ".rb"
-
1
loader.push_dir __dir__
-
1
loader.setup
-
end
-
-
# Main namespace.
-
1
module Cogger
-
1
extend Registry
-
-
1
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%L%:z"
-
1
LEVELS = %w[debug info warn error fatal unknown].freeze
-
-
1
def self.loader registry = Zeitwerk::Registry
-
4
@loader ||= registry.loaders.each.find { |loader| loader.tag == File.basename(__FILE__, ".rb") }
-
end
-
-
1
def self.new(...) = Hub.new(...)
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "logger"
-
1
require "refinements/array"
-
-
1
module Cogger
-
# Defines the default configuration for all pipes.
-
1
Configuration = Data.define(
-
:id,
-
:io,
-
:level,
-
:formatter,
-
:datetime_format,
-
:tags,
-
:header,
-
:mode,
-
:age,
-
:size,
-
:suffix,
-
:entry,
-
:logger,
-
:mutex
-
) do
-
1
using Refinements::Array
-
-
1
def initialize id: Program.call,
-
io: $stdout,
-
level: Level.call,
-
formatter: Formatters::Emoji.new,
-
datetime_format: DATETIME_FORMAT,
-
tags: Core::EMPTY_ARRAY,
-
header: true,
-
mode: false,
-
age: nil,
-
size: 1_048_576,
-
suffix: "%Y-%m-%d",
-
entry: Entry,
-
logger: Logger,
-
mutex: Mutex.new
-
158
super.tap { tags.freeze }
-
end
-
-
52
then: 7
else: 44
def entag(other = nil) = other ? tags.including(other) : tags
-
-
1
def to_logger
-
71
logger.new io,
-
age,
-
size,
-
progname: id,
-
level:,
-
formatter:,
-
datetime_format:,
-
skip_header: skip_header?,
-
binmode: mode,
-
shift_period_suffix: suffix
-
end
-
-
1
def inspect
-
8
"#<#{self.class} @id=#{id}, @io=#{io.class}, @level=#{level}, " \
-
"@formatter=#{formatter.class}, @datetime_format=#{datetime_format.inspect}, " \
-
"@tags=#{tags.inspect}, @header=#{header}, @mode=#{mode}, @age=#{age}, @size=#{size}, " \
-
"@suffix=#{suffix.inspect}, @entry=#{entry}, @logger=#{logger}>"
-
end
-
-
1
private
-
-
1
def skip_header? = formatter == :json || formatter.is_a?(Formatters::JSON) || !header
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Cogger
-
# Defines a log entry which can be formatted for output.
-
1
Entry = Data.define :id, :level, :at, :message, :tags, :datetime_format, :payload do
-
1
def self.for(message = nil, **payload, &)
-
91
then: 21
else: 70
content = block_given? ? yield : message
-
-
91
new id: payload.delete(:id) || Program.call,
-
91
level: (payload.delete(:level) || "INFO").upcase,
-
at: payload.delete(:at) || ::Time.now,
-
message: sanitize!(content, payload),
-
tags: Array(payload.delete(:tags)),
-
datetime_format: payload.delete(:datetime_format) || DATETIME_FORMAT,
-
payload:
-
end
-
-
1
def self.for_crash message, error, id:
-
10
new id:,
-
level: "FATAL",
-
message:,
-
payload: {
-
error_message: error.message,
-
error_class: error.class,
-
backtrace: error.backtrace
-
}
-
end
-
-
1
def self.sanitize! content, payload
-
91
then: 3
body = if content.is_a? Hash
-
6
content.delete(:message).tap { payload.merge! content }
-
else: 88
else
-
88
content
-
end
-
-
91
then: 52
if body.is_a? String
-
52
body.encode "UTF-8", invalid: :replace, undef: :replace, replace: "?"
-
else: 39
else
-
39
body
-
end
-
end
-
-
1
private_class_method :sanitize!
-
-
1
def initialize id: Program.call,
-
level: "INFO",
-
at: ::Time.now,
-
message: nil,
-
tags: Core::EMPTY_ARRAY,
-
datetime_format: DATETIME_FORMAT,
-
payload: Core::EMPTY_HASH
-
108
super
-
end
-
-
1
def attributes = {id:, level:, at:, message:, **payload}
-
-
1
def tagged_attributes tagger: Tag
-
22
computed_tags = tagger.for(*tags)
-
-
22
then: 15
else: 7
return attributes if computed_tags.empty?
-
-
7
{id:, level:, at:, message:, **computed_tags.to_h, **payload}
-
end
-
-
1
def tagged tagger: Tag
-
66
attributes.tap do |pairs|
-
66
computed_tags = tagger.for(*tags)
-
66
else: 65
then: 1
pairs[:message] = "#{computed_tags} #{pairs[:message]}" unless computed_tags.empty?
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Formatters
-
# An abstract class with common/shared functionality.
-
1
class Abstract
-
1
NEW_LINE = "\n"
-
-
SANITIZERS = {
-
1
escape: Sanitizers::Escape.new,
-
filter: Sanitizers::Filter,
-
format_time: Sanitizers::FormatTime
-
}.freeze
-
-
1
def initialize sanitizers: SANITIZERS
-
109
@sanitizers = sanitizers
-
end
-
-
1
def call(*)
-
1
fail NoMethodError,
-
"`#{self.class}##{__method__} #{method(__method__).parameters}` must be implemented."
-
end
-
-
1
protected
-
-
1
def sanitize entry, method
-
84
entry.public_send(method).tap do |attributes|
-
84
filter attributes
-
506
attributes.transform_values! { |value| format_time value, format: entry.datetime_format }
-
end
-
end
-
-
45
def escape(...) = (@escape ||= sanitizers.fetch(__method__)).call(...)
-
-
85
def filter(...) = (@filter ||= sanitizers.fetch(__method__)).call(...)
-
-
423
def format_time(...) = (@format_time ||= sanitizers.fetch(__method__)).call(...)
-
-
1
private
-
-
1
attr_reader :sanitizers
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Formatters
-
# Formats by color.
-
1
class Color < Abstract
-
1
TEMPLATE = "<dynamic>[%<id>s]</dynamic> %<message:dynamic>s"
-
-
1
def initialize template = TEMPLATE, parser: Parsers::Combined.new
-
63
super()
-
63
@template = template
-
63
@parser = parser
-
end
-
-
1
def call(*input)
-
45
*, entry = input
-
45
attributes = sanitize entry, :tagged
-
-
45
format(parse(attributes[:level]), attributes).tap(&:strip!) << NEW_LINE
-
end
-
-
1
private
-
-
1
attr_reader :template, :parser
-
-
1
def parse(level) = parser.call template, level
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Formatters
-
# Formats fatal crashes.
-
1
class Crash < Abstract
-
1
TEMPLATE = <<~CONTENT
-
<dynamic>[%<id>s] [%<level>s] [%<at>s] Crash!
-
%<message>s
-
%<error_message>s (%<error_class>s)
-
%<backtrace>s</dynamic>
-
CONTENT
-
-
1
def initialize template = TEMPLATE, parser: Parsers::Combined.new
-
9
super()
-
9
@template = template
-
9
@parser = parser
-
end
-
-
1
def call(*input)
-
9
*, entry = input
-
9
attributes = sanitize entry, :tagged
-
9
attributes[:backtrace] = %( #{attributes[:backtrace].join "\n "})
-
-
9
format(parse(attributes[:level]), attributes) << NEW_LINE
-
end
-
-
1
private
-
-
1
attr_reader :template, :parser
-
-
1
def parse(level) = parser.call template, level
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Formatters
-
# Formats by emoji and color.
-
1
class Emoji < Color
-
1
TEMPLATE = "%<emoji:dynamic>s <dynamic>[%<id>s]</dynamic> %<message:dynamic>s"
-
-
1
def initialize(template = TEMPLATE, ...)
-
54
super
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "json"
-
-
1
module Cogger
-
1
module Formatters
-
# Formats as JSON output.
-
1
class JSON < Abstract
-
1
TEMPLATE = nil
-
-
1
def initialize template = TEMPLATE, parser: Parsers::Position.new
-
16
super()
-
16
@template = template
-
16
@parser = parser
-
end
-
-
1
def call(*input)
-
11
*, entry = input
-
11
attributes = sanitize(entry, :tagged_attributes).tap(&:compact!)
-
-
11
parser.call(template, attributes).to_json << NEW_LINE
-
end
-
-
1
private
-
-
1
attr_reader :template, :parser
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Formatters
-
1
module Parsers
-
# An abstract class with common functionality.
-
1
class Abstract
-
1
TRANSFORMERS = {color: Transformers::Color.new, emoji: Transformers::Emoji.new}.freeze
-
-
1
def initialize registry: Cogger, transformers: TRANSFORMERS, expressor: Regexp
-
41
@registry = registry
-
41
@transformers = transformers
-
41
@expressor = expressor
-
end
-
-
1
def call(_template, **)
-
1
fail NoMethodError,
-
"`#{self.class}##{__method__} #{method(__method__).parameters}` must be implemented."
-
end
-
-
1
protected
-
-
1
attr_reader :registry, :transformers, :expressor
-
-
141
def transform_color(...) = (@tranform_color ||= transformers.fetch(:color)).call(...)
-
-
45
def transform_emoji(...) = (@transform_emoji ||= transformers.fetch(:emoji)).call(...)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Formatters
-
1
module Parsers
-
# Parses template literals, emojis, and keys for specific and dynamic colors.
-
1
class Combined
-
1
STEPS = [Element.new, Emoji.new, Key.new].freeze # Order matters.
-
-
1
def initialize steps: STEPS
-
74
@steps = steps
-
end
-
-
1
def call template, level
-
224
steps.reduce(template.dup) { |modification, step| step.call modification, level }
-
end
-
-
1
private
-
-
1
attr_reader :steps
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Formatters
-
1
module Parsers
-
# Parses template elements for specific and dynamic colors.
-
1
class Element < Abstract
-
1
PATTERN = %r(
-
< # Tag open start.
-
(?<directive>\w+) # Tag open name.
-
> # Tag open end.
-
(?<content>.+?) # Content.
-
</ # Tag close start.
-
\w+ # Tag close.
-
> # Tag close end.
-
)mx
-
-
1
def initialize pattern: PATTERN
-
8
super()
-
8
@pattern = pattern
-
end
-
-
1
def call template, level
-
63
mutate template, level
-
63
template
-
end
-
-
1
private
-
-
1
attr_reader :pattern
-
-
1
def mutate template, level
-
63
template.gsub! pattern do
-
58
captures = expressor.last_match.named_captures
-
58
transform_color captures["content"], captures["directive"], level
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Formatters
-
1
module Parsers
-
# Parses template emojis for specific and dynamic colors.
-
1
class Emoji < Abstract
-
1
PATTERN = /
-
%< # Start.
-
(?<key>emoji) # Key.
-
: # Delimiter.
-
(?<directive>\w+) # Directive.
-
>s # End.
-
/x
-
-
1
def initialize pattern: PATTERN
-
5
super()
-
5
@pattern = pattern
-
end
-
-
1
def call template, level
-
60
mutate template, level
-
60
template
-
end
-
-
1
private
-
-
1
attr_reader :pattern
-
-
1
def mutate template, level
-
60
template.gsub! pattern do
-
44
captures = expressor.last_match.named_captures
-
44
transform_emoji captures["key"], captures["directive"], level
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Cogger
-
1
module Formatters
-
1
module Parsers
-
# Parses template for specific and dynamic keys (i.e. string format specifiers).
-
1
class Key < Abstract
-
1
PATTERN = /
-
% # Start.
-
(?<flag>[\s#+-0*])? # Optional flag.
-
\.? # Optional precision.
-
(?<width>\d+)? # Optional width.
-
< # Reference start.
-
(?<key>\w+) # Key.
-
: # Delimiter.
-
(?<directive>\w+) # Directive.
-
> # Reference end.
-
(?<specifier>[ABEGXabcdefgiopsux]) # Specifier.
-
/x
-
-
1
def initialize pattern: PATTERN
-
27
super()
-
27
@pattern = pattern
-
end
-
-
1
def call template, level
-
82
mutate template, level
-
82
template
-
end
-
-
1
private
-
-
1
attr_reader :pattern
-
-
1
def mutate template, level
-
82
template.gsub! pattern do |match|
-
82
captures = expressor.last_match.named_captures
-
82
directive = captures["directive"]
-
82
match.sub! ":#{directive}", Core::EMPTY_STRING
-
-
82
transform_color match, directive, level
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Formatters
-
1
module Parsers
-
# Parses template and reorders attributes based on template key positions.
-
1
class Position
-
1
PATTERN = /
-
% # Start.
-
? # Flag, width, or precision.
-
< # Reference start.
-
(?<name>\w+) # Name.
-
(?::\w+)? # Optional delimiter and directive.
-
> # Reference end.
-
? # Specifier.
-
/x
-
-
1
def initialize pattern: PATTERN
-
33
@pattern = pattern
-
end
-
-
1
def call template, attributes
-
28
else: 11
then: 17
return attributes unless String(template).match? pattern
-
-
11
keys = scan template
-
11
attributes.slice(*keys).merge!(attributes.except(*keys))
-
end
-
-
1
private
-
-
1
attr_reader :pattern
-
-
35
def scan(template) = template.scan(pattern).map { |match| match.first.to_sym }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Cogger
-
1
module Formatters
-
# Formats as key=value output.
-
1
class Property < Abstract
-
1
TEMPLATE = nil
-
-
1
def initialize template = TEMPLATE, parser: Parsers::Position.new
-
9
super()
-
9
@template = template
-
9
@parser = parser
-
end
-
-
1
def call(*input)
-
9
*, entry = input
-
9
attributes = sanitize(entry, :tagged_attributes).tap(&:compact!)
-
-
9
concat(attributes).chop! << NEW_LINE
-
end
-
-
1
private
-
-
1
attr_reader :template, :parser
-
-
1
def concat attributes
-
9
parser.call(template, attributes).each.with_object(+"") do |(key, value), line|
-
44
line << key.to_s << "=" << escape(value) << " "
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Formatters
-
1
module Sanitizers
-
# Sanitizes value as fully quoted string for emojis, spaces, and control characters.
-
1
class Escape
-
1
PATTERN = /
-
\A # Search string start.
-
.* # Match zero or more characters.
-
( # Conditional start.
-
(?!\p{Number}) # Look ahead and ignore unicode numbers.
-
\p{Emoji} # Match unicode emoji only.
-
| # Or.
-
[[:space:]] # Match spaces, tabs, and new lines.
-
| # Or.
-
[[:cntrl:]] # Match control characters.
-
) # Conditional end.
-
.* # Match zero or more characters.
-
\z # Search string end.
-
/xu
-
-
1
def initialize pattern: PATTERN
-
10
@pattern = pattern
-
end
-
-
1
def call value
-
53
else: 2
then: 51
return dump value unless value.is_a? Array
-
-
7
value.reduce(+"") { |text, item| text << dump(item) << ", " }
-
2
.then { |text| %([#{text.delete_suffix ", "}]).dump }
-
end
-
-
1
private
-
-
1
attr_reader :pattern
-
-
1
def dump(value) = value.to_s.gsub(pattern, &:dump)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Formatters
-
1
module Sanitizers
-
# Sanitizes/removes sensitive values.
-
1
Filter = lambda do |attributes, filters: Cogger.filters|
-
164
then: 3
else: 72
filters.each { |key| attributes[key] = "[FILTERED]" if attributes.key? key }
-
89
attributes
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "date"
-
-
1
module Cogger
-
1
module Formatters
-
1
module Sanitizers
-
# Sanitizes/formats date/time value.
-
1
FormatTime = lambda do |value, format: Cogger::DATETIME_FORMAT|
-
428
else: 89
then: 339
return value unless value.is_a?(::Time) || value.is_a?(Date) || value.is_a?(DateTime)
-
-
89
value.strftime format
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Formatters
-
# Formats simple templates that require minimal processing.
-
1
class Simple < Abstract
-
1
TEMPLATE = "[%<id>s] %<message>s"
-
-
1
def initialize template = TEMPLATE
-
11
super()
-
11
@template = template
-
end
-
-
1
def call(*input)
-
10
*, entry = input
-
10
attributes = sanitize entry, :tagged
-
-
10
format(template, attributes).tap(&:strip!) << NEW_LINE
-
end
-
-
1
private
-
-
1
attr_reader :template, :processor
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Formatters
-
1
module Transformers
-
# Transforms target into colorized string.
-
1
class Color
-
1
def initialize emoji: Emoji::KEY, key_transformer: Key, registry: Cogger
-
6
@emoji = emoji
-
6
@key_transformer = key_transformer
-
6
@registry = registry
-
end
-
-
1
def call target, directive, level
-
145
then: 2
else: 143
return target if !target.is_a?(String) || target == emoji
-
-
143
key = key_transformer.call directive, level
-
-
143
then: 142
else: 1
return client.encode target, key if aliases.key?(key) || defaults.key?(key)
-
-
1
target
-
end
-
-
1
private
-
-
1
attr_reader :emoji, :key_transformer, :registry
-
-
1
def aliases = registry.aliases
-
-
1
def defaults = client.defaults
-
-
1
def client = registry.color
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Formatters
-
1
module Transformers
-
# Transforms target into emoji.
-
1
class Emoji
-
1
KEY = "emoji"
-
-
1
def initialize key = KEY, key_transformer: Key, registry: Cogger
-
5
@key = key
-
5
@key_transformer = key_transformer
-
5
@registry = registry
-
end
-
-
1
def call target, directive, level
-
48
else: 47
then: 1
return target unless target == key
-
-
47
key = key_transformer.call directive, level
-
-
47
then: 46
else: 1
registry.aliases.key?(key) ? registry.get_emoji(key) : target
-
end
-
-
1
private
-
-
1
attr_reader :key, :key_transformer, :registry
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Formatters
-
1
module Transformers
-
# Transforms directive, based on log level, into a key for color or emoji lookup.
-
194
then: 160
else: 33
Key = -> directive, level { (directive == "dynamic" ? level.downcase : directive).to_sym }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "forwardable"
-
1
require "logger"
-
1
require "refinements/hash"
-
-
1
module Cogger
-
# Loads configuration and simultaneously sends messages to multiple streams.
-
# :reek:TooManyMethods
-
1
class Hub
-
1
extend Forwardable
-
-
1
using Refinements::Hash
-
1
using Refines::Logger
-
-
1
delegate %i[
-
close
-
reopen
-
debug!
-
debug?
-
info!
-
info?
-
warn!
-
warn?
-
error!
-
error?
-
fatal!
-
fatal?
-
formatter
-
formatter=
-
level
-
level=
-
] => :primary
-
-
1
delegate %i[id io tags mode age size suffix] => :configuration
-
-
1
def initialize(registry: Cogger, model: Configuration, **attributes)
-
57
@registry = registry
-
57
@configuration = model[**find_formatter(attributes)]
-
57
@primary = configuration.to_logger
-
57
@streams = [@primary]
-
end
-
-
1
def add_stream **attributes
-
1
attributes[:id] = configuration.id
-
1
streams.append configuration.with(**find_formatter(attributes)).to_logger
-
1
self
-
end
-
-
1
def debug(message = nil, **, &) = log(__method__, message, **, &)
-
-
1
def info(message = nil, **, &) = log(__method__, message, **, &)
-
-
1
def warn(message = nil, **, &) = log(__method__, message, **, &)
-
-
1
def error(message = nil, **, &) = log(__method__, message, **, &)
-
-
1
def fatal(message = nil, **, &) = log(__method__, message, **, &)
-
-
1
def any(message = nil, **, &) = log(__method__, message, **, &)
-
-
1
def abort(message = nil, **payload, &block)
-
4
then: 3
else: 1
error(message, **payload, &block) if message || !payload.empty? || block
-
4
exit false
-
end
-
-
1
def add(level, message = nil, **, &)
-
3
log(Logger::SEV_LABEL.fetch(level, "ANY").downcase, message, **, &)
-
end
-
-
1
alias unknown any
-
-
1
def reread = primary.reread
-
-
1
def inspect
-
7
%(#<#{self.class} #{configuration.inspect.delete_prefix! "#<Cogger::Configuration "})
-
end
-
-
1
private
-
-
1
attr_reader :registry, :configuration, :primary, :streams
-
-
# :reek:FeatureEnvy
-
1
def find_formatter attributes
-
58
attributes.transform_value! :formatter do |value|
-
16
else: 8
then: 8
next value unless value.is_a?(Symbol) || value.is_a?(String)
-
-
8
formatter, template = registry.get_formatter value
-
8
then: 1
else: 7
template ? formatter.new(template) : formatter.new
-
end
-
end
-
-
1
def log(level, message = nil, **, &)
-
46
dispatch(level, message, **, &)
-
rescue StandardError => error
-
7
crash message, error
-
end
-
-
# rubocop:todo Metrics/MethodLength
-
1
def dispatch(level, message, **payload, &)
-
46
entry = configuration.entry.for(
-
message,
-
id: configuration.id,
-
level:,
-
tags: configuration.entag(payload.delete(:tags)),
-
datetime_format: configuration.datetime_format,
-
**payload,
-
&
-
)
-
-
138
configuration.mutex.synchronize { streams.each { |logger| logger.public_send level, entry } }
-
39
true
-
end
-
# rubocop:enable Metrics/MethodLength
-
-
1
def crash message, error
-
7
configuration.with(id: :cogger, io: $stdout, formatter: Formatters::Crash.new)
-
.to_logger
-
.fatal configuration.entry.for_crash(message, error, id: configuration.id)
-
7
true
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "logger"
-
1
require "refinements/array"
-
-
# Loads log level from environment.
-
1
module Cogger
-
1
using Refinements::Array
-
-
1
Level = lambda do |logger = Logger, environment: ENV, allowed: LEVELS|
-
36
value = String environment.fetch("LOG_LEVEL", "INFO")
-
-
36
then: 35
else: 1
return logger.const_get value.upcase if allowed.include? value.downcase
-
-
1
fail ArgumentError, %(Invalid log level: #{value.inspect}. Use: #{allowed.to_usage "or"}.)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "pathname"
-
-
# Computes default program name based on current file name.
-
1
module Cogger
-
1
Program = lambda do |name = $PROGRAM_NAME|
-
250
Pathname(name).then { |path| path.basename(path.extname).to_s }
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Cogger
-
1
module Rack
-
# Middlware for enriched logging based on the incoming request.
-
1
class Logger
-
DEFAULTS = {
-
1
logger: Cogger.new(formatter: :json),
-
timer: Cogger::Time::Span.new,
-
key_map: {
-
verb: "REQUEST_METHOD",
-
ip: "REMOTE_ADDR",
-
path: "PATH_INFO",
-
params: "QUERY_STRING",
-
length: "CONTENT_LENGTH"
-
}
-
}.freeze
-
-
1
def initialize application, options = Core::EMPTY_HASH, defaults: DEFAULTS
-
4
configuration = defaults.merge options
-
-
4
@application = application
-
4
@logger = configuration.fetch :logger
-
4
@timer = configuration.fetch :timer
-
4
@key_map = configuration.fetch :key_map
-
end
-
-
1
def call environment
-
4
request = ::Rack::Request.new environment
-
8
(status, headers, body), duration, unit = timer.call { application.call environment }
-
-
4
logger.info tags: [tags_for(request), {status:, duration:, unit:}]
-
-
4
[status, headers, body]
-
end
-
-
1
private
-
-
1
attr_reader :application, :logger, :timer, :key_map
-
-
1
def tags_for request
-
4
key_map.each_key.with_object({}) do |tag, collection|
-
20
key = key_map.fetch tag, tag
-
20
collection[String(tag).downcase] = request.get_header key
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "logger"
-
1
require "refinements/string_io"
-
-
1
module Cogger
-
1
module Refines
-
# Provides additional enhancements to a log device.
-
1
module LogDevice
-
1
using Refinements::StringIO
-
-
1
refine ::Logger::LogDevice do
-
1
def reread
-
11
when: 2
case dev
-
2
when: 7
when ::File then dev.class.new(dev).read
-
7
else: 2
when ::StringIO then dev.reread
-
2
else ""
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Refines
-
# Provides additional enhancements to a logger.
-
1
module Logger
-
1
using LogDevice
-
-
1
refine ::Logger do
-
1
def reread = @logdev.reread
-
-
1
alias_method :any, :unknown
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/hash"
-
1
require "tone"
-
-
1
module Cogger
-
# Provides a global regsitry for global configuration.
-
1
module Registry
-
1
using Refinements::Hash
-
-
1
def self.extended descendant
-
38
descendant.add_alias(:debug, :white)
-
.add_alias(:info, :green)
-
.add_alias(:warn, :yellow)
-
.add_alias(:error, :red)
-
.add_alias(:fatal, :bold, :white, :on_red)
-
.add_alias(:any, :dim, :bright_white)
-
.add_emojis(
-
debug: "๐",
-
info: "๐ข",
-
warn: "โ ๏ธ",
-
error: "๐",
-
fatal: "๐ฅ",
-
any: "โซ๏ธ"
-
)
-
.add_formatter(:color, Cogger::Formatters::Color)
-
.add_formatter(
-
:detail,
-
Cogger::Formatters::Simple,
-
"[%<id>s] [%<level>s] [%<at>s] %<message>s"
-
)
-
.add_formatter(:emoji, Cogger::Formatters::Emoji)
-
.add_formatter(:json, Cogger::Formatters::JSON)
-
.add_formatter(:property, Cogger::Formatters::Property)
-
.add_formatter(:simple, Cogger::Formatters::Simple)
-
.add_formatter :rack,
-
Cogger::Formatters::Simple,
-
"[%<id>s] [%<level>s] [%<at>s] %<verb>s %<status>s " \
-
"%<duration>s %<ip>s %<path>s %<length>s %<params>s"
-
end
-
-
1
def add_alias(key, *styles)
-
230
color.add_alias(key, *styles)
-
230
self
-
end
-
-
1
def aliases = color.aliases
-
-
1
def add_emoji key, value
-
4
warn "`#{self.class}##{__method__}` is deprecated, use `#add_emojis` instead.",
-
category: :deprecated
-
-
4
emojis[key.to_sym] = value
-
4
self
-
end
-
-
1
def add_emojis(**attributes)
-
44
emojis.merge! attributes.symbolize_keys!
-
44
self
-
end
-
-
1
def get_emoji key
-
52
emojis.fetch(key.to_sym) { fail KeyError, "Unregistered emoji: #{key}." }
-
end
-
-
1
def emojis = @emojis ||= {}
-
-
1
def add_filter key
-
4
warn "`#{self.class}##{__method__}` is deprecated, use `#add_filters` instead.",
-
category: :deprecated
-
-
4
filters.add key.to_sym
-
4
self
-
end
-
-
1
def add_filters(*keys)
-
5
filters.merge(keys.map(&:to_sym))
-
5
self
-
end
-
-
1
def filters = @filters ||= Set.new
-
-
1
def add_formatter key, formatter, template = nil
-
274
formatters[key.to_sym] = [formatter, template || formatter::TEMPLATE]
-
273
self
-
rescue NameError
-
1
raise NameError, "#{formatter}::TEMPLATE must be defined with a default template string."
-
end
-
-
1
def get_formatter key
-
16
formatters.fetch(key.to_sym) { fail KeyError, "Unregistered formatter: #{key}." }
-
end
-
-
1
def formatters = @formatters ||= {}
-
-
1
def templates
-
1
formatters.each.with_object({}) do |(key, (_formatter, template)), collection|
-
7
collection[key] = template
-
end
-
end
-
-
1
def color = @color ||= Tone.new
-
-
1
def defaults = {emojis:, aliases:, formatters:, filters:, color:}
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "refinements/hash"
-
-
1
module Cogger
-
# Models a tag which may consist of an array and/or hash.
-
1
Tag = Data.define :singles, :pairs do
-
1
using Refinements::Hash
-
-
1
def self.for(*bag)
-
93
bag.each.with_object new do |item, tag|
-
34
then: 3
else: 31
value = item.is_a?(Proc) ? item.call : item
-
34
then: 17
else: 17
value.is_a?(Hash) ? tag.pairs.merge!(value) : tag.singles.append(value)
-
end
-
end
-
-
1
def initialize singles: [], pairs: {}
-
116
super
-
end
-
-
1
def empty? = singles.empty? && pairs.empty?
-
-
12
then: 1
else: 10
def to_h = empty? ? Core::EMPTY_HASH : {tags: singles.to_a, **pairs}.tap(&:compress!)
-
-
6
then: 1
else: 4
def to_s = empty? ? Core::EMPTY_STRING : "#{format_singles} #{format_pairs}".tap(&:strip!)
-
-
1
private
-
-
1
def format_singles
-
11
singles.map { |value| "[#{value}]" }
-
.join " "
-
end
-
-
1
def format_pairs
-
8
pairs.map { |key, value| "[#{key}=#{value}]" }
-
.join(" ")
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Time
-
# An adapter for acquiring current time.
-
12
Clock = -> id = Process::CLOCK_MONOTONIC, unit: :nanosecond { Process.clock_gettime id, unit }
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Time
-
RANGE = {
-
1
nanoseconds: ...1_000,
-
microseconds: 1_000...1_000_000,
-
milliseconds: 1_000_000...1_000_000_000,
-
seconds: 1_000_000_000...60_000_000_000,
-
minutes: 60_000_000_000...
-
}.freeze
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Time
-
# Measures duration of a process with nanosecond precision.
-
1
class Span
-
1
def initialize clock = Clock, unit: Unit, range: RANGE
-
6
@clock = clock
-
6
@unit = unit
-
6
@range = range
-
end
-
-
1
def call
-
9
start = current
-
9
result = yield
-
9
span = current - start
-
-
9
[result, duration(span), unit.call(span)]
-
end
-
-
1
private
-
-
1
attr_reader :clock, :unit, :range
-
-
1
def duration value
-
9
when: 1
case value
-
1
when: 5
when nanoseconds then value
-
5
when: 1
when microseconds then value / microseconds.min
-
1
when: 1
when milliseconds then value / milliseconds.min
-
1
else: 1
when seconds then value / seconds.min
-
1
else value / minutes.min
-
end
-
end
-
-
1
def current = clock.call
-
-
1
def nanoseconds
-
9
@nanoseconds ||= range.fetch __method__
-
end
-
-
1
def microseconds
-
13
@microseconds ||= range.fetch __method__
-
end
-
-
1
def milliseconds
-
4
@milliseconds ||= range.fetch __method__
-
end
-
-
1
def seconds
-
3
@seconds ||= range.fetch __method__
-
end
-
-
1
def minutes
-
1
@minutes ||= range.fetch __method__
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Cogger
-
1
module Time
-
# Provides unit of measure for duration.
-
1
Unit = lambda do |duration, range: RANGE|
-
14
when: 2
case duration
-
2
when: 6
when range.fetch(:nanoseconds) then "ns"
-
6
when: 2
when range.fetch(:microseconds) then "ยตs"
-
2
when: 2
when range.fetch(:milliseconds) then "ms"
-
2
else: 2
when range.fetch(:seconds) then "s"
-
2
else "m"
-
end
-
end
-
end
-
end