This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2024, by Samuel Williams.
|
||||
|
||||
require_relative "terminal"
|
||||
require_relative "serialized"
|
||||
require_relative "failure"
|
||||
|
||||
module Console
|
||||
module Output
|
||||
module Default
|
||||
def self.new(output, **options)
|
||||
output ||= $stderr
|
||||
|
||||
if output.tty?
|
||||
output = Terminal.new(output, **options)
|
||||
else
|
||||
output = Serialized.new(output, **options)
|
||||
end
|
||||
|
||||
return output
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2024-2025, by Samuel Williams.
|
||||
|
||||
require_relative "wrapper"
|
||||
require_relative "../event/failure"
|
||||
|
||||
module Console
|
||||
module Output
|
||||
# A wrapper for outputting failure messages, which can include exceptions.
|
||||
class Failure < Wrapper
|
||||
def initialize(output, **options)
|
||||
super(output, **options)
|
||||
end
|
||||
|
||||
# The exception must be either the last argument or passed as an option.
|
||||
def call(subject = nil, *arguments, exception: nil, **options, &block)
|
||||
if exception.nil?
|
||||
last = arguments.last
|
||||
if last.is_a?(Exception)
|
||||
options[:event] = Event::Failure.for(last)
|
||||
end
|
||||
elsif exception.is_a?(Exception)
|
||||
options[:event] = Event::Failure.for(exception)
|
||||
else
|
||||
# We don't know what this is, so we just pass it through:
|
||||
options[:exception] = exception
|
||||
end
|
||||
|
||||
super(subject, *arguments, **options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2023-2024, by Samuel Williams.
|
||||
|
||||
module Console
|
||||
module Output
|
||||
class Null
|
||||
def initialize(...)
|
||||
end
|
||||
|
||||
def last_output
|
||||
self
|
||||
end
|
||||
|
||||
def call(...)
|
||||
# Do nothing.
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,105 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2024, by Samuel Williams.
|
||||
|
||||
require_relative "wrapper"
|
||||
|
||||
module Console
|
||||
module Output
|
||||
class Sensitive < Wrapper
|
||||
REDACT = /
|
||||
phone
|
||||
| email
|
||||
| full_?name
|
||||
| first_?name
|
||||
| last_?name
|
||||
|
||||
| device_name
|
||||
| user_agent
|
||||
|
||||
| zip
|
||||
| address
|
||||
| location
|
||||
| latitude
|
||||
| longitude
|
||||
|
||||
| ip
|
||||
| gps
|
||||
|
||||
| sex
|
||||
| gender
|
||||
|
||||
| token
|
||||
| password
|
||||
/xi
|
||||
|
||||
def initialize(output, redact: REDACT, **options)
|
||||
super(output, **options)
|
||||
|
||||
@redact = redact
|
||||
end
|
||||
|
||||
def redact?(text)
|
||||
text.match?(@redact)
|
||||
end
|
||||
|
||||
def redact_hash(arguments, filter)
|
||||
arguments.transform_values do |value|
|
||||
redact(value, filter)
|
||||
end
|
||||
end
|
||||
|
||||
def redact_array(array, filter)
|
||||
array.map do |value|
|
||||
redact(value, filter)
|
||||
end
|
||||
end
|
||||
|
||||
def redact(argument, filter)
|
||||
case argument
|
||||
when String
|
||||
if filter
|
||||
filter.call(argument)
|
||||
elsif redact?(argument)
|
||||
"[REDACTED]"
|
||||
else
|
||||
argument
|
||||
end
|
||||
when Array
|
||||
redact_array(argument, filter)
|
||||
when Hash
|
||||
redact_hash(argument, filter)
|
||||
else
|
||||
redact(argument.to_s, filter)
|
||||
end
|
||||
end
|
||||
|
||||
class Filter
|
||||
def initialize(substitutions)
|
||||
@substitutions = substitutions
|
||||
@pattern = Regexp.union(substitutions.keys)
|
||||
end
|
||||
|
||||
def call(text)
|
||||
text.gsub(@pattern, @substitutions)
|
||||
end
|
||||
end
|
||||
|
||||
def call(subject = nil, *arguments, sensitive: true, **options, &block)
|
||||
if sensitive
|
||||
if sensitive.respond_to?(:call)
|
||||
filter = sensitive
|
||||
elsif sensitive.is_a?(Hash)
|
||||
filter = Filter.new(sensitive)
|
||||
end
|
||||
|
||||
subject = redact(subject, filter)
|
||||
arguments = redact_array(arguments, filter)
|
||||
end
|
||||
|
||||
super(subject, *arguments, **options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,77 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2019-2025, by Samuel Williams.
|
||||
|
||||
require_relative "../format"
|
||||
require "time"
|
||||
require "fiber/annotation"
|
||||
|
||||
module Console
|
||||
module Output
|
||||
class Serialized
|
||||
def initialize(io, format: Format.default, **options)
|
||||
@io = io
|
||||
@format = format
|
||||
end
|
||||
|
||||
# This a final output that then writes to an IO object.
|
||||
def last_output
|
||||
self
|
||||
end
|
||||
|
||||
attr :io
|
||||
attr :format
|
||||
|
||||
def dump(record)
|
||||
@format.dump(record)
|
||||
end
|
||||
|
||||
def call(subject = nil, *arguments, severity: UNKNOWN, **options, &block)
|
||||
record = {
|
||||
time: Time.now.iso8601,
|
||||
severity: severity,
|
||||
oid: subject.object_id,
|
||||
pid: Process.pid,
|
||||
}
|
||||
|
||||
# We want to log just a brief subject:
|
||||
if subject.is_a?(String)
|
||||
record[:subject] = subject
|
||||
elsif subject.is_a?(Module)
|
||||
record[:subject] = subject.name
|
||||
else
|
||||
record[:subject] = subject.class.name
|
||||
end
|
||||
|
||||
if annotation = Fiber.current.annotation
|
||||
record[:annotation] = annotation
|
||||
end
|
||||
|
||||
message = arguments
|
||||
|
||||
if block_given?
|
||||
if block.arity.zero?
|
||||
message << yield
|
||||
else
|
||||
buffer = StringIO.new
|
||||
yield buffer
|
||||
message << buffer.string
|
||||
end
|
||||
end
|
||||
|
||||
if message.size == 1
|
||||
record[:message] = message.first
|
||||
elsif message.any?
|
||||
record[:message] = message
|
||||
end
|
||||
|
||||
record.update(options)
|
||||
|
||||
@io.write(self.dump(record) << "\n")
|
||||
end
|
||||
end
|
||||
|
||||
JSON = Serialized
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2022-2024, by Samuel Williams.
|
||||
|
||||
module Console
|
||||
module Output
|
||||
class Split
|
||||
def self.[](*outputs)
|
||||
self.new(outputs)
|
||||
end
|
||||
|
||||
def initialize(outputs)
|
||||
@outputs = outputs
|
||||
end
|
||||
|
||||
def verbose!(value = true)
|
||||
@outputs.each{|output| output.verbose!(value)}
|
||||
end
|
||||
|
||||
def call(...)
|
||||
@outputs.each do |output|
|
||||
output.call(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,257 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2019-2024, by Samuel Williams.
|
||||
# Copyright, 2021, by Robert Schulze.
|
||||
|
||||
require_relative "../clock"
|
||||
require_relative "../terminal"
|
||||
|
||||
require "json"
|
||||
require "fiber"
|
||||
require "fiber/annotation"
|
||||
require "stringio"
|
||||
|
||||
module Console
|
||||
module Output
|
||||
class Terminal
|
||||
class Buffer < StringIO
|
||||
def initialize(prefix = nil)
|
||||
@prefix = prefix
|
||||
|
||||
super()
|
||||
end
|
||||
|
||||
attr :prefix
|
||||
|
||||
def puts(*args, prefix: @prefix)
|
||||
args.each do |arg|
|
||||
self.write(prefix) if prefix
|
||||
super(arg)
|
||||
end
|
||||
end
|
||||
|
||||
alias << puts
|
||||
end
|
||||
|
||||
# This, and all related methods, is considered private.
|
||||
CONSOLE_START_AT = "CONSOLE_START_AT"
|
||||
|
||||
# Exports CONSOLE_START which can be used to synchronize the start times of all child processes when they log using delta time.
|
||||
def self.start_at!(environment = ENV)
|
||||
if time_string = environment[CONSOLE_START_AT]
|
||||
start_at = Time.parse(time_string) rescue nil
|
||||
end
|
||||
|
||||
unless start_at
|
||||
start_at = Time.now
|
||||
environment[CONSOLE_START_AT] = start_at.to_s
|
||||
end
|
||||
|
||||
return start_at
|
||||
end
|
||||
|
||||
def initialize(output, verbose: nil, start_at: Terminal.start_at!, format: nil, **options)
|
||||
@io = output
|
||||
@start_at = start_at
|
||||
|
||||
@terminal = format.nil? ? Console::Terminal.for(@io) : format.new(@io)
|
||||
|
||||
if verbose.nil?
|
||||
@verbose = !@terminal.colors?
|
||||
else
|
||||
@verbose = verbose
|
||||
end
|
||||
|
||||
@terminal[:logger_suffix] ||= @terminal.style(:white, nil, :faint)
|
||||
@terminal[:subject] ||= @terminal.style(nil, nil, :bold)
|
||||
@terminal[:debug] = @terminal.style(:cyan)
|
||||
@terminal[:info] = @terminal.style(:green)
|
||||
@terminal[:warn] = @terminal.style(:yellow)
|
||||
@terminal[:error] = @terminal.style(:red)
|
||||
@terminal[:fatal] = @terminal[:error]
|
||||
|
||||
@terminal[:annotation] = @terminal.reset
|
||||
@terminal[:value] = @terminal.style(:blue)
|
||||
|
||||
@formatters = {}
|
||||
self.register_formatters
|
||||
end
|
||||
|
||||
# This a final output that then writes to an IO object.
|
||||
def last_output
|
||||
self
|
||||
end
|
||||
|
||||
attr :io
|
||||
|
||||
attr_accessor :verbose
|
||||
|
||||
attr :start
|
||||
attr :terminal
|
||||
|
||||
def verbose!(value = true)
|
||||
@verbose = value
|
||||
end
|
||||
|
||||
def register_formatters(namespace = Console::Terminal::Formatter)
|
||||
namespace.constants.each do |name|
|
||||
formatter = namespace.const_get(name)
|
||||
@formatters[formatter::KEY] = formatter.new(@terminal)
|
||||
end
|
||||
end
|
||||
|
||||
UNKNOWN = :unknown
|
||||
|
||||
def call(subject = nil, *arguments, name: nil, severity: UNKNOWN, event: nil, **options, &block)
|
||||
width = @terminal.width
|
||||
|
||||
prefix = build_prefix(name || severity.to_s)
|
||||
indent = " " * prefix.size
|
||||
|
||||
buffer = Buffer.new("#{indent}| ")
|
||||
indent_size = buffer.prefix.size
|
||||
|
||||
format_subject(severity, prefix, subject, buffer)
|
||||
|
||||
arguments.each do |argument|
|
||||
format_argument(argument, buffer)
|
||||
end
|
||||
|
||||
if block_given?
|
||||
if block.arity.zero?
|
||||
format_argument(yield, buffer)
|
||||
else
|
||||
yield(buffer, @terminal)
|
||||
end
|
||||
end
|
||||
|
||||
if event
|
||||
format_event(event, buffer, width - indent_size)
|
||||
end
|
||||
|
||||
if options&.any?
|
||||
format_options(options, buffer)
|
||||
end
|
||||
|
||||
@io.write buffer.string
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def format_event(event, buffer, width)
|
||||
event = event.to_hash
|
||||
type = event[:type]
|
||||
|
||||
if formatter = @formatters[type]
|
||||
formatter.format(event, buffer, verbose: @verbose, width: width)
|
||||
else
|
||||
format_value(::JSON.pretty_generate(event), buffer)
|
||||
end
|
||||
end
|
||||
|
||||
def format_options(options, output)
|
||||
format_value(::JSON.pretty_generate(options), output)
|
||||
end
|
||||
|
||||
def format_argument(argument, output)
|
||||
argument.to_s.each_line do |line|
|
||||
output.puts line
|
||||
end
|
||||
end
|
||||
|
||||
def format_subject(severity, prefix, subject, buffer)
|
||||
if subject.is_a?(String)
|
||||
format_string_subject(severity, prefix, subject, buffer)
|
||||
elsif subject.is_a?(Module)
|
||||
format_string_subject(severity, prefix, subject.to_s, buffer)
|
||||
else
|
||||
format_object_subject(severity, prefix, subject, buffer)
|
||||
end
|
||||
end
|
||||
|
||||
def default_suffix(object = nil)
|
||||
buffer = +""
|
||||
|
||||
if @verbose
|
||||
if annotation = Fiber.current.annotation
|
||||
# While typically annotations should be strings, that is not always the case.
|
||||
annotation = annotation.to_s
|
||||
|
||||
# If the annotation is empty, we don't want to print it, as it will look like a formatting bug.
|
||||
if annotation.size > 0
|
||||
buffer << ": #{@terminal[:annotation]}#{annotation}#{@terminal.reset}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
buffer << " #{@terminal[:logger_suffix]}"
|
||||
|
||||
if object
|
||||
buffer << "[oid=0x#{object.object_id.to_s(16)}] "
|
||||
end
|
||||
|
||||
buffer << "[ec=0x#{Fiber.current.object_id.to_s(16)}] [pid=#{Process.pid}] [#{::Time.now}]#{@terminal.reset}"
|
||||
|
||||
return buffer
|
||||
end
|
||||
|
||||
def format_object_subject(severity, prefix, subject, output)
|
||||
prefix_style = @terminal[severity]
|
||||
|
||||
if @verbose
|
||||
suffix = default_suffix(subject)
|
||||
end
|
||||
|
||||
prefix = "#{prefix_style}#{prefix}:#{@terminal.reset} "
|
||||
|
||||
output.puts "#{@terminal[:subject]}#{subject.class}#{@terminal.reset}#{suffix}", prefix: prefix
|
||||
end
|
||||
|
||||
def format_string_subject(severity, prefix, subject, output)
|
||||
prefix_style = @terminal[severity]
|
||||
|
||||
if @verbose
|
||||
suffix = default_suffix
|
||||
end
|
||||
|
||||
prefix = "#{prefix_style}#{prefix}:#{@terminal.reset} "
|
||||
|
||||
output.puts "#{@terminal[:subject]}#{subject}#{@terminal.reset}#{suffix}", prefix: prefix
|
||||
end
|
||||
|
||||
def format_value(value, output)
|
||||
string = value.to_s
|
||||
|
||||
string.each_line do |line|
|
||||
line.chomp!
|
||||
output.puts "#{@terminal[:value]}#{line}#{@terminal.reset}"
|
||||
end
|
||||
end
|
||||
|
||||
def time_offset_prefix
|
||||
Clock.formatted_duration(Time.now - @start_at).rjust(6)
|
||||
end
|
||||
|
||||
def build_prefix(name)
|
||||
if @verbose
|
||||
"#{time_offset_prefix} #{name.rjust(8)}"
|
||||
else
|
||||
time_offset_prefix
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Text
|
||||
def self.new(output, **options)
|
||||
Terminal.new(output, format: Console::Terminal::Text, **options)
|
||||
end
|
||||
end
|
||||
|
||||
module XTerm
|
||||
def self.new(output, **options)
|
||||
Terminal.new(output, format: Console::Terminal::XTerm, **options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2024, by Samuel Williams.
|
||||
|
||||
module Console
|
||||
module Output
|
||||
class Wrapper
|
||||
def initialize(delegate, **options)
|
||||
@delegate = delegate
|
||||
end
|
||||
|
||||
attr :delegate
|
||||
|
||||
def last_output
|
||||
@delegate.last_output
|
||||
end
|
||||
|
||||
def verbose!(value = true)
|
||||
@delegate.verbose!(value)
|
||||
end
|
||||
|
||||
def call(...)
|
||||
@delegate.call(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user