pvincent
3 months ago
15 changed files with 514 additions and 493 deletions
-
2Procfile.dev
-
5config/application.rb
-
7config/command_detection.rb
-
11config/environments/development.rb
-
2config/initializers/monkey_patcher.rb
-
58lib/formatters/ansi_colors.rb
-
20lib/formatters/ansi_dimensions.rb
-
245lib/formatters/ansi_formatter.rb
-
80lib/formatters/ansi_wrapper.rb
-
82lib/formatters/basic_formatter.rb
-
60lib/semantic/ansi_colors.rb
-
22lib/semantic/ansi_dimensions.rb
-
247lib/semantic/ansi_formatter.rb
-
82lib/semantic/ansi_wrapper.rb
-
84lib/semantic/basic_formatter.rb
@ -1,3 +1,3 @@ |
|||
|
|||
web: RUBY_DEBUG_OPEN=true bundle exec -- rails server --port "${RAILS_PORT:-7500}" |
|||
css: BROWSERSLIST_IGNORE_OLD_DATA=true bundle exec -- rails tailwindcss:watch |
|||
css: bundle exec -- rails tailwindcss:watch |
@ -0,0 +1,7 @@ |
|||
# useful methods for figuring out which kind of process is running |
|||
module CommandDetection |
|||
def server? = Rails.const_defined?('Server') |
|||
def console? = !server? && defined?(Rails::Console) == 'constant' |
|||
def rake? = !server? && !console? && Rails.const_defined?('Rake') |
|||
def tailwind_watcher? = rake? && Rake.application.top_level_tasks.first == 'tailwindcss:watch' |
|||
end |
@ -1,58 +0,0 @@ |
|||
# common definitions and constants |
|||
module AnsiColors |
|||
ANSI_REGEX = /\e\[[0-9;]*m/ # TODO: support for \x1b and \033 |
|||
|
|||
# FORMAT |
|||
CLEAR = "\e[0m".freeze |
|||
BOLD = "\e[1m".freeze |
|||
UNDERLINE = "\e[4m".freeze |
|||
|
|||
# TEXT |
|||
TEXT_BLACK = "\e[30m".freeze |
|||
TEXT_RED = "\e[31m".freeze |
|||
TEXT_GREEN = "\e[32m".freeze |
|||
TEXT_YELLOW = "\e[33m".freeze |
|||
TEXT_BLUE = "\e[34m".freeze |
|||
TEXT_MAGENTA = "\e[35m".freeze |
|||
TEXT_CYAN = "\e[36m".freeze |
|||
TEXT_WHITE = "\e[37m".freeze |
|||
|
|||
# TEXT GRAY SHADES |
|||
TEXT_GRAY_800 = "\e[38;5;232m".freeze |
|||
TEXT_GRAY_700 = "\e[38;5;233m".freeze |
|||
TEXT_GRAY_600 = "\e[38;5;235m".freeze |
|||
TEXT_GRAY_500 = "\e[38;5;238m".freeze |
|||
TEXT_GRAY_400 = "\e[38;5;241m".freeze |
|||
TEXT_GRAY_300 = "\e[38;5;244m".freeze |
|||
TEXT_GRAY_200 = "\e[38;5;249m".freeze |
|||
TEXT_GRAY_100 = "\e[38;5;252m".freeze |
|||
|
|||
# DARK TEXT |
|||
DARK_TEXT_BLACK = "\e[90m".freeze |
|||
DARK_TEXT_RED = "\e[91m".freeze |
|||
DARK_TEXT_GREEN = "\e[92m".freeze |
|||
DARK_TEXT_YELLOW = "\e[93m".freeze |
|||
DARK_TEXT_BLUE = "\e[94m".freeze |
|||
DARK_TEXT_MAGENTA = "\e[95m".freeze |
|||
DARK_TEXT_CYAN = "\e[96m".freeze |
|||
DARK_TEXT_WHITE = "\e[97m".freeze |
|||
|
|||
# BACKGROUND |
|||
BG_BLACK = "\e[40m".freeze |
|||
BG_WHITE = "\e[47m".freeze |
|||
BG_GRAY = "\e[100m".freeze |
|||
BG_RED = "\e[41m".freeze |
|||
BG_GREEN = "\e[42m".freeze |
|||
BG_YELLOW = "\e[43m".freeze |
|||
BG_BLUE = "\e[44m".freeze |
|||
BG_MAGENTA = "\e[45m".freeze |
|||
BG_CYAN = "\e[46m".freeze |
|||
|
|||
# DARK BACKGROUND |
|||
DARK_BG_RED = "\e[101m".freeze |
|||
DARK_BG_GREEN = "\e[102m".freeze |
|||
DARK_BG_YELLOW = "\e[103m".freeze |
|||
DARK_BG_BLUE = "\e[104m".freeze |
|||
DARK_BG_MAGENTA = "\e[105m".freeze |
|||
DARK_BG_CYAN = "\e[106m".freeze |
|||
end |
@ -1,20 +0,0 @@ |
|||
require 'ostruct' |
|||
|
|||
# extra dimensions for customizing the logging format |
|||
module AnsiDimensions |
|||
def self.new(rails: '╣x╠', before: 0, after: 0, terminus: false) |
|||
OpenStruct.new(rails:, before:, after:, terminus:) # rubocop:disable Style/OpenStructUse |
|||
end |
|||
|
|||
def self.start |
|||
OpenStruct.new(rails: '╓─╖') # rubocop:disable Style/OpenStructUse |
|||
end |
|||
|
|||
def self.end |
|||
OpenStruct.new(rails: '╣ ╠') # rubocop:disable Style/OpenStructUse |
|||
end |
|||
|
|||
def self.around |
|||
OpenStruct.new(rails: '╣ ╠') # rubocop:disable Style/OpenStructUse |
|||
end |
|||
end |
@ -1,245 +0,0 @@ |
|||
require_relative 'ansi_wrapper' |
|||
require_relative 'ansi_colors' |
|||
require_relative 'ansi_dimensions' |
|||
|
|||
require 'io/console' |
|||
require 'amazing_print' |
|||
require 'json' |
|||
|
|||
# wraps meanwhile takes care of ansi colors |
|||
class AnsiFormatter < SemanticLogger::Formatters::Color |
|||
include AnsiColors |
|||
|
|||
CENTER_SIZE = 20 |
|||
FOREMAN_PREFIX_LENGTH = 18 |
|||
FAILOVER_WRAP = 80 |
|||
CHAR_FATAL = '⯶'.freeze |
|||
TERMINUS_STRING = '╙─╜'.freeze |
|||
RENDERED_VIEW_DURATION = 100 |
|||
TOTAL_RENDERED_VIEW_DURATION = RENDERED_VIEW_DURATION * 5 |
|||
|
|||
def initialize |
|||
super(color_map: |
|||
ColorMap.new( |
|||
debug: CLEAR + TEXT_GRAY_400, |
|||
info: CLEAR + TEXT_GRAY_100, |
|||
warn: CLEAR + BG_YELLOW + TEXT_BLACK, |
|||
error: CLEAR + BG_RED + TEXT_WHITE, |
|||
fatal: CLEAR + BG_MAGENTA + BOLD + TEXT_WHITE |
|||
)) |
|||
@memory = nil |
|||
end |
|||
|
|||
def call(log, logger) |
|||
log = alter(log) |
|||
|
|||
self.log = log |
|||
self.logger = logger |
|||
self.color = color_map[log.level] |
|||
|
|||
wrap_level(compute_useful_length, message, payload, exception) |
|||
end |
|||
|
|||
def reject(log) |
|||
return true if log.name == 'ActionView::Base' && log.message&.starts_with?(' Rendering') |
|||
|
|||
true if log.name == 'Rails' && log.message&.starts_with?('Loaded') |
|||
end |
|||
|
|||
private |
|||
|
|||
def build_regex_redirected |
|||
dev_port = Rails.application.routes.default_url_options[:port] |
|||
dev_hosts = ['127.0.0.1', 'localhost', Rails.application.routes.default_url_options[:host]].uniq.join('|') |
|||
dev_from = "http://(?:#{dev_hosts}):#{dev_port}" |
|||
regex_s = "^(Redirected to )#{dev_from}(.*)" |
|||
Regexp.new(regex_s) |
|||
end |
|||
|
|||
def ansi_trace(trace, symbol) |
|||
match = trace.match(/(↳ )?(.*:\d+)(:in `)?(.*'?)/) # only m2(=file) and m4(=optional function) are useful |
|||
return trace unless match |
|||
|
|||
_, m2, _, m4 = match.captures |
|||
"#{symbol} #{m2} #{BOLD}#{m4.chop}#{CLEAR}" |
|||
end |
|||
|
|||
def two_captures_last_as_bold(message, regex) |
|||
match = message.match(regex) |
|||
return "unmatched: #{message}" unless match |
|||
|
|||
m1, m2 = match.captures |
|||
"#{m1}#{BOLD}#{m2}#{CLEAR}" |
|||
end |
|||
|
|||
def alter(log) |
|||
if log.name == 'Rails' |
|||
if log.message |
|||
log.message.lstrip! |
|||
log.message.chomp!('') |
|||
if log.message.starts_with?('Started') |
|||
rails = '╓─╖' |
|||
before = 1 |
|||
if @memory |
|||
rails = "╓#{@memory}╖" |
|||
before = 0 |
|||
end |
|||
log.dimensions = AnsiDimensions.new(rails:, before:) |
|||
@memory = nil |
|||
log.message = two_captures_last_as_bold(log.message, /(^Started \w* )"(.*?)"/) |
|||
elsif log.message.starts_with?('Parameters') |
|||
parameters = log.message.match(/Parameters: ({.*}$)/).match(1) |
|||
parameters = JSON.parse(parameters.gsub('=>', ':'), symbolize_names: true) |
|||
parameters = parameters.ai(ruby19_syntax: true, plain: true, multiline: false) |
|||
parameters = parameters.gsub(/\w*:/) { |key| "#{TEXT_GRAY_200}#{key.chop}#{color_map[:debug]}:" } |
|||
parameters = parameters.gsub(/: ".*?"/) do |value| |
|||
": \"#{TEXT_YELLOW}#{value[3..-2]}#{color_map[:debug]}\"" |
|||
end |
|||
log.message = "Parameters: #{parameters}" |
|||
log.level = :debug |
|||
elsif log.message.starts_with?('Completed') |
|||
m1, m2, m3, m4 = log.message.match(/^Completed (\d+) (.*) in (\d+\.?\d*)ms (.*)$/).captures |
|||
http_code = m1.to_i |
|||
duration = m3.to_f |
|||
duration = duration > TOTAL_RENDERED_VIEW_DURATION ? " in #{m3}ms #{m4}" : '' |
|||
log.message = "Completed #{BOLD}#{http_code}#{CLEAR} #{m2}#{duration}" |
|||
if http_code / 100 == 2 |
|||
log.dimensions = AnsiDimensions.new(rails: TERMINUS_STRING, after: 1) |
|||
elsif http_code / 100 == 3 |
|||
@memory = '║' |
|||
log.dimensions = AnsiDimensions.new(rails: "╙#{@memory}╜") |
|||
elsif http_code / 100 == 4 |
|||
log.dimensions = AnsiDimensions.new(rails: '╙╨╜') |
|||
elsif http_code / 100 == 5 |
|||
log.dimensions = AnsiDimensions.new(rails: "╙#{draw_fatal}╜") |
|||
end |
|||
elsif log.message =~ /^(Processing|Parameters)/ |
|||
log.level = :debug |
|||
if log.message =~ /^Processing/ |
|||
log.message = two_captures_last_as_bold(log.message, /(^Processing by \w*#\w* as )(.*)/) |
|||
end |
|||
elsif log.message =~ /Redirected/ |
|||
log.level = :debug |
|||
@regex_redirected ||= build_regex_redirected # lazy building |
|||
log.message = two_captures_last_as_bold(log.message, @regex_redirected) |
|||
end |
|||
elsif log.exception |
|||
log.dimensions = AnsiDimensions.new( |
|||
rails: "╓#{draw_fatal(log.level.to_s.chr.upcase)}╖", |
|||
after: 1, |
|||
terminus: true |
|||
) |
|||
end |
|||
elsif log.name =~ /^(ActionView|ActiveRecord)::Base/ |
|||
log.level = :debug |
|||
log.message.lstrip! |
|||
if log.name == 'ActiveRecord::Base' |
|||
unbold!(log.message) |
|||
|
|||
if log.message.starts_with?('↳ ') |
|||
log.message = ansi_trace(log.message, '⇄') |
|||
else |
|||
sql_match = log.message.match(/\s+\[\[.*\]\]$/) |
|||
if sql_match |
|||
sql_command = sql_match.pre_match |
|||
sql_type, sql_entry = sql_command.split("\e[0m") |
|||
sql_color_entry = sql_entry.match(ANSI_REGEX).to_s |
|||
sql_args = JSON.parse(sql_match.to_s).map(&:last) |
|||
sql_args.each_with_index do |val, index| |
|||
sql_arg = val.inspect |
|||
sql_arg = "'#{val.gsub("'", "''")}'" if val.is_a?(String) |
|||
sql_entry.gsub!("$#{index + 1}", colorize(sql_arg, BOLD) + sql_color_entry) |
|||
end |
|||
log.message = [sql_type, sql_entry].join(CLEAR) |
|||
end |
|||
end |
|||
elsif log.name == 'ActionView::Base' |
|||
match = log.message.match(/^Rendered( layout| collection of|) (.*?\.erb)(.*)(\(Duration:.*\))/) |
|||
if match |
|||
m1, m2, m3, m4 = match.captures |
|||
duration = m4.match(/Duration: (\d+\.?\d*)ms/).match(1).to_f |
|||
duration = duration < RENDERED_VIEW_DURATION ? '' : m4.to_s |
|||
log.message = "⇤ Rendered#{m1} app/views/#{m2}#{m3}#{duration}" |
|||
end |
|||
end |
|||
|
|||
end |
|||
|
|||
log |
|||
end |
|||
|
|||
def draw_fatal(char = CHAR_FATAL) |
|||
BG_MAGENTA + BOLD + TEXT_WHITE + char + CLEAR |
|||
end |
|||
|
|||
def origin = colorize(centerize(log.name), TEXT_CYAN) |
|||
def build_prefix(char) = "#{origin} ╣#{colorize(char)}╠ " |
|||
def build_terminus = "#{origin} #{TERMINUS_STRING} " |
|||
def centerize(text) = text.truncate(CENTER_SIZE).center(CENTER_SIZE) |
|||
def colorize(text, tint = color) = "#{tint}#{text}#{CLEAR}" |
|||
|
|||
def stackisize(items) |
|||
return '' if items.empty? |
|||
|
|||
traces = items.map { |item| ansi_trace(item, '➟') } |
|||
"\n#{traces.join("\n")}" |
|||
end |
|||
|
|||
def build_dimensions(dimensions) |
|||
"#{origin} #{dimensions.rails} " |
|||
end |
|||
|
|||
def compute_useful_length |
|||
IO.console.winsize[1] - FOREMAN_PREFIX_LENGTH |
|||
rescue StandardError |
|||
FAILOVER_WRAP |
|||
end |
|||
|
|||
def unbold!(text) = text.gsub!(BOLD, '') |
|||
|
|||
def message |
|||
colorize(log.message) if log.message |
|||
end |
|||
|
|||
def payload |
|||
return unless log.payload |
|||
|
|||
log.payload.ai(indent: 2, object_id: false) |
|||
end |
|||
|
|||
def exception |
|||
return unless log.exception |
|||
|
|||
exc = log.exception |
|||
clazz = colorize("#{exc.class}\n", color_map[:fatal]) |
|||
message = colorize(exc.message.chomp(''), color_map[:error]) |
|||
backtrace = stackisize(Rails.backtrace_cleaner.clean(exc.backtrace)) |
|||
|
|||
"#{clazz}#{message}#{backtrace}" |
|||
end |
|||
|
|||
def level_char |
|||
case log.level |
|||
when :info, :debug then ' ' |
|||
else log.level.to_s.chr.upcase |
|||
end |
|||
end |
|||
|
|||
def wrap_level(length, *items) |
|||
prefix = log.dimensions ? build_dimensions(log.dimensions) : build_prefix(level_char) |
|||
continuation = build_prefix('┆') |
|||
|
|||
result = items.map do |item| |
|||
AnsiWrapper.wrap(item, length, prefix, continuation) |
|||
end |
|||
|
|||
if log.dimensions&.terminus |
|||
terminus = AnsiWrapper.wrap(' ', length, build_terminus) |
|||
result << terminus |
|||
end |
|||
|
|||
log.dimensions&.before&.times { result.unshift('') } |
|||
log.dimensions&.after&.times { result << '' } |
|||
result.compact.join("\n") |
|||
end |
|||
end |
@ -1,80 +0,0 @@ |
|||
require_relative 'ansi_colors' |
|||
|
|||
# AnsiWrapper cares about Ansi Colour Code \e[... |
|||
class AnsiWrapper |
|||
include AnsiColors |
|||
|
|||
TAB_TO_SPACES = 2 |
|||
|
|||
def self.wrap(text, length, prefix = '', continuation = prefix) |
|||
if visible_length(prefix) != visible_length(continuation) |
|||
raise "continuation <#{continuation.inspect}> should have the same length as prefix <#{prefix.inspect}>" |
|||
end |
|||
return unless text |
|||
|
|||
text = text.gsub("\t", ' ' * TAB_TO_SPACES) |
|||
|
|||
lines = split_text_to_lines(text, length - visible_length(prefix)) |
|||
lines = inject_continuation_and_ansi_colors_to_lines(lines, prefix, continuation) |
|||
lines.join("\n") |
|||
end |
|||
|
|||
private_class_method def self.inject_continuation_and_ansi_colors_to_lines(lines, prefix, continuation) |
|||
last_ansi = '' |
|||
lines.each_with_index.map do |line, index| |
|||
current = index.zero? ? prefix : continuation |
|||
current += last_ansi unless last_ansi.empty? || last_ansi == CLEAR |
|||
current += line |
|||
|
|||
last_ansi = scan_for_actual_ansi(line, last_ansi) |
|||
|
|||
current += CLEAR if last_ansi.empty? || last_ansi != CLEAR |
|||
current |
|||
end |
|||
end |
|||
|
|||
private_class_method def self.scan_for_actual_ansi(line, last_ansi) |
|||
line.scan(ANSI_REGEX).each do |match| |
|||
ansi_code = match.to_s |
|||
if ansi_code == CLEAR |
|||
last_ansi = CLEAR |
|||
else |
|||
last_ansi += ansi_code |
|||
end |
|||
end |
|||
last_ansi |
|||
end |
|||
|
|||
private_class_method def self.split_text_to_lines(text, length) |
|||
lines = text.split("\n") |
|||
sublines = lines.map do |line| |
|||
visible_length(line) > length ? visible_split(line, length) : [line] |
|||
end |
|||
sublines.flatten |
|||
end |
|||
|
|||
private_class_method def self.visible_length(line) |
|||
raise 'line should not contain carriage return character!' if line.match "\n" |
|||
|
|||
ansi_code_length = line.scan(ANSI_REGEX).map(&:length).sum |
|||
line.length - ansi_code_length |
|||
end |
|||
|
|||
# TODO: might be refactored with less complexity |
|||
private_class_method def self.visible_split(line, length, stack = '') # rubocop:disable Metrics/AbcSize,Metrics/MethodLength |
|||
before, ansi_code, after = line.partition(ANSI_REGEX) |
|||
stack_length = visible_length(stack) |
|||
visible_length = before.length + stack_length |
|||
if visible_length == length |
|||
["#{stack}#{before}#{ansi_code}"] + visible_split(after, length) |
|||
elsif visible_length > length |
|||
first_line = stack + before[0...length - stack_length] |
|||
tail = before[length - stack_length..] + ansi_code + after |
|||
[first_line] + visible_split(tail, length) |
|||
elsif ansi_code.length.positive? |
|||
visible_split(after, length, "#{stack}#{before}#{ansi_code}") |
|||
else |
|||
["#{stack}#{before}#{ansi_code}"] |
|||
end |
|||
end |
|||
end |
@ -1,82 +0,0 @@ |
|||
# My Custom colorized formatter |
|||
class BasicFormatter < SemanticLogger::Formatters::Color |
|||
ANSI_REVERSED_WARNING = "\e[0;30;43m".freeze |
|||
ANSI_REVERSED_ERROR = "\e[1;30;41m".freeze |
|||
ANSI_GRAY = "\e[90m".freeze |
|||
# Return the complete log level name in uppercase |
|||
|
|||
def initialize |
|||
super(time_format: '%H:%M:%S', |
|||
color_map: ColorMap.new( |
|||
debug: ANSI_GRAY, |
|||
info: SemanticLogger::AnsiColors::GREEN, |
|||
warn: SemanticLogger::AnsiColors::YELLOW |
|||
)) |
|||
@time_format = nil if File.exist?(File.join(Rails.root, 'Procfile.dev')) |
|||
end |
|||
|
|||
def time |
|||
"#{color}#{format_time(log.time)}#{color_map.clear}" if time_format |
|||
end |
|||
|
|||
def message |
|||
return unless log.message |
|||
|
|||
prefix = "#{color}--#{color_map.clear}" |
|||
|
|||
case log.level |
|||
when :info |
|||
message = log.message |
|||
message = message&.rstrip if log.name == 'Rails' && message.starts_with?('Completed') |
|||
if log.name == 'Rails' && message.starts_with?('Started') |
|||
message = message.split('for')[0] |
|||
puts '' if Rails.env.development? |
|||
end |
|||
if log.name == 'Rails' || log.name == 'ActionView::Base' |
|||
"#{prefix} #{ANSI_GRAY}#{message}#{color_map.clear}" |
|||
else |
|||
"#{prefix} #{SemanticLogger::AnsiColors::WHITE}#{message}#{color_map.clear}" |
|||
end |
|||
when :warn |
|||
"#{prefix} #{ANSI_REVERSED_WARNING}#{log.message}#{color_map.clear}" |
|||
when :error, :fatal |
|||
"#{prefix} #{ANSI_REVERSED_ERROR}#{log.message}#{color_map.clear}" |
|||
else |
|||
"#{prefix} #{color}#{log.message}#{color_map.clear}" |
|||
end |
|||
end |
|||
|
|||
def tags; end |
|||
|
|||
def process_info |
|||
fname = file_name_and_line |
|||
"#{color}[#{fname}]#{color_map.clear}" if fname |
|||
end |
|||
|
|||
def exception |
|||
return unless log.exception |
|||
|
|||
root_path = Rails.root.to_s |
|||
backtrace = log.exception.backtrace.select do |line| |
|||
line.starts_with?(root_path) |
|||
end |
|||
|
|||
if backtrace.count.positive? |
|||
"-- #{ANSI_REVERSED_ERROR}#{log.exception.class}#{color_map.clear} #{color}#{log.exception.message}#{color_map.clear}#{SemanticLogger::AnsiColors::WHITE}\n\t#{backtrace.join("\n\t")}#{color_map.clear}\n\n" |
|||
else |
|||
"-- #{ANSI_REVERSED_ERROR}#{log.exception.class}: #{log.exception.message}#{color_map.clear}\n\n" |
|||
end |
|||
end |
|||
|
|||
def call(log, logger) |
|||
self.color = color_map[log.level] |
|||
self.log = log |
|||
self.logger = logger |
|||
|
|||
if @time_format |
|||
[time, level, process_info, tags, named_tags, duration, name, message, payload, exception].compact.join(' ') |
|||
else |
|||
[tags, named_tags, duration, name, message, payload, exception].compact.join(' ') |
|||
end |
|||
end |
|||
end |
@ -0,0 +1,60 @@ |
|||
module Semantic |
|||
# common definitions and constants |
|||
module AnsiColors |
|||
ANSI_REGEX = /\e\[[0-9;]*m/ # TODO: support for \x1b and \033 |
|||
|
|||
# FORMAT |
|||
CLEAR = "\e[0m".freeze |
|||
BOLD = "\e[1m".freeze |
|||
UNDERLINE = "\e[4m".freeze |
|||
|
|||
# TEXT |
|||
TEXT_BLACK = "\e[30m".freeze |
|||
TEXT_RED = "\e[31m".freeze |
|||
TEXT_GREEN = "\e[32m".freeze |
|||
TEXT_YELLOW = "\e[33m".freeze |
|||
TEXT_BLUE = "\e[34m".freeze |
|||
TEXT_MAGENTA = "\e[35m".freeze |
|||
TEXT_CYAN = "\e[36m".freeze |
|||
TEXT_WHITE = "\e[37m".freeze |
|||
|
|||
# TEXT GRAY SHADES |
|||
TEXT_GRAY_800 = "\e[38;5;232m".freeze |
|||
TEXT_GRAY_700 = "\e[38;5;233m".freeze |
|||
TEXT_GRAY_600 = "\e[38;5;235m".freeze |
|||
TEXT_GRAY_500 = "\e[38;5;238m".freeze |
|||
TEXT_GRAY_400 = "\e[38;5;241m".freeze |
|||
TEXT_GRAY_300 = "\e[38;5;244m".freeze |
|||
TEXT_GRAY_200 = "\e[38;5;249m".freeze |
|||
TEXT_GRAY_100 = "\e[38;5;252m".freeze |
|||
|
|||
# DARK TEXT |
|||
DARK_TEXT_BLACK = "\e[90m".freeze |
|||
DARK_TEXT_RED = "\e[91m".freeze |
|||
DARK_TEXT_GREEN = "\e[92m".freeze |
|||
DARK_TEXT_YELLOW = "\e[93m".freeze |
|||
DARK_TEXT_BLUE = "\e[94m".freeze |
|||
DARK_TEXT_MAGENTA = "\e[95m".freeze |
|||
DARK_TEXT_CYAN = "\e[96m".freeze |
|||
DARK_TEXT_WHITE = "\e[97m".freeze |
|||
|
|||
# BACKGROUND |
|||
BG_BLACK = "\e[40m".freeze |
|||
BG_WHITE = "\e[47m".freeze |
|||
BG_GRAY = "\e[100m".freeze |
|||
BG_RED = "\e[41m".freeze |
|||
BG_GREEN = "\e[42m".freeze |
|||
BG_YELLOW = "\e[43m".freeze |
|||
BG_BLUE = "\e[44m".freeze |
|||
BG_MAGENTA = "\e[45m".freeze |
|||
BG_CYAN = "\e[46m".freeze |
|||
|
|||
# DARK BACKGROUND |
|||
DARK_BG_RED = "\e[101m".freeze |
|||
DARK_BG_GREEN = "\e[102m".freeze |
|||
DARK_BG_YELLOW = "\e[103m".freeze |
|||
DARK_BG_BLUE = "\e[104m".freeze |
|||
DARK_BG_MAGENTA = "\e[105m".freeze |
|||
DARK_BG_CYAN = "\e[106m".freeze |
|||
end |
|||
end |
@ -0,0 +1,22 @@ |
|||
require 'ostruct' |
|||
|
|||
module Semantic |
|||
# extra dimensions for customizing the logging format |
|||
module AnsiDimensions |
|||
def self.new(rails: '╣x╠', before: 0, after: 0, terminus: false) |
|||
OpenStruct.new(rails:, before:, after:, terminus:) # rubocop:disable Style/OpenStructUse |
|||
end |
|||
|
|||
def self.start |
|||
OpenStruct.new(rails: '╓─╖') # rubocop:disable Style/OpenStructUse |
|||
end |
|||
|
|||
def self.end |
|||
OpenStruct.new(rails: '╣ ╠') # rubocop:disable Style/OpenStructUse |
|||
end |
|||
|
|||
def self.around |
|||
OpenStruct.new(rails: '╣ ╠') # rubocop:disable Style/OpenStructUse |
|||
end |
|||
end |
|||
end |
@ -0,0 +1,247 @@ |
|||
require_relative 'ansi_wrapper' |
|||
require_relative 'ansi_colors' |
|||
require_relative 'ansi_dimensions' |
|||
|
|||
require 'io/console' |
|||
require 'amazing_print' |
|||
require 'json' |
|||
|
|||
module Semantic |
|||
# wraps meanwhile takes care of ansi colors |
|||
class AnsiFormatter < SemanticLogger::Formatters::Color |
|||
include AnsiColors |
|||
|
|||
CENTER_SIZE = 20 |
|||
FOREMAN_PREFIX_LENGTH = 18 |
|||
FAILOVER_WRAP = 80 |
|||
CHAR_FATAL = '⯶'.freeze |
|||
TERMINUS_STRING = '╙─╜'.freeze |
|||
RENDERED_VIEW_DURATION = 100 |
|||
TOTAL_RENDERED_VIEW_DURATION = RENDERED_VIEW_DURATION * 5 |
|||
|
|||
def initialize |
|||
super(color_map: |
|||
ColorMap.new( |
|||
debug: CLEAR + TEXT_GRAY_400, |
|||
info: CLEAR + TEXT_GRAY_100, |
|||
warn: CLEAR + BG_YELLOW + TEXT_BLACK, |
|||
error: CLEAR + BG_RED + TEXT_WHITE, |
|||
fatal: CLEAR + BG_MAGENTA + BOLD + TEXT_WHITE |
|||
)) |
|||
@memory = nil |
|||
end |
|||
|
|||
def call(log, logger) |
|||
log = alter(log) |
|||
|
|||
self.log = log |
|||
self.logger = logger |
|||
self.color = color_map[log.level] |
|||
|
|||
wrap_level(compute_useful_length, message, payload, exception) |
|||
end |
|||
|
|||
def reject(log) |
|||
return true if log.name == 'ActionView::Base' && log.message&.starts_with?(' Rendering') |
|||
|
|||
true if log.name == 'Rails' && log.message&.starts_with?('Loaded') |
|||
end |
|||
|
|||
private |
|||
|
|||
def build_regex_redirected |
|||
dev_port = Rails.application.routes.default_url_options[:port] |
|||
dev_hosts = ['127.0.0.1', 'localhost', Rails.application.routes.default_url_options[:host]].uniq.join('|') |
|||
dev_from = "http://(?:#{dev_hosts}):#{dev_port}" |
|||
regex_s = "^(Redirected to )#{dev_from}(.*)" |
|||
Regexp.new(regex_s) |
|||
end |
|||
|
|||
def ansi_trace(trace, symbol) |
|||
match = trace.match(/(↳ )?(.*:\d+)(:in `)?(.*'?)/) # only m2(=file) and m4(=optional function) are useful |
|||
return trace unless match |
|||
|
|||
_, m2, _, m4 = match.captures |
|||
"#{symbol} #{m2} #{BOLD}#{m4.chop}#{CLEAR}" |
|||
end |
|||
|
|||
def two_captures_last_as_bold(message, regex) |
|||
match = message.match(regex) |
|||
return "unmatched: #{message}" unless match |
|||
|
|||
m1, m2 = match.captures |
|||
"#{m1}#{BOLD}#{m2}#{CLEAR}" |
|||
end |
|||
|
|||
def alter(log) |
|||
if log.name == 'Rails' |
|||
if log.message |
|||
log.message.lstrip! |
|||
log.message.chomp!('') |
|||
if log.message.starts_with?('Started') |
|||
rails = '╓─╖' |
|||
before = 1 |
|||
if @memory |
|||
rails = "╓#{@memory}╖" |
|||
before = 0 |
|||
end |
|||
log.dimensions = AnsiDimensions.new(rails:, before:) |
|||
@memory = nil |
|||
log.message = two_captures_last_as_bold(log.message, /(^Started \w* )"(.*?)"/) |
|||
elsif log.message.starts_with?('Parameters') |
|||
parameters = log.message.match(/Parameters: ({.*}$)/).match(1) |
|||
parameters = JSON.parse(parameters.gsub('=>', ':'), symbolize_names: true) |
|||
parameters = parameters.ai(ruby19_syntax: true, plain: true, multiline: false) |
|||
parameters = parameters.gsub(/\w*:/) { |key| "#{TEXT_GRAY_200}#{key.chop}#{color_map[:debug]}:" } |
|||
parameters = parameters.gsub(/: ".*?"/) do |value| |
|||
": \"#{TEXT_YELLOW}#{value[3..-2]}#{color_map[:debug]}\"" |
|||
end |
|||
log.message = "Parameters: #{parameters}" |
|||
log.level = :debug |
|||
elsif log.message.starts_with?('Completed') |
|||
m1, m2, m3, m4 = log.message.match(/^Completed (\d+) (.*) in (\d+\.?\d*)ms (.*)$/).captures |
|||
http_code = m1.to_i |
|||
duration = m3.to_f |
|||
duration = duration > TOTAL_RENDERED_VIEW_DURATION ? " in #{m3}ms #{m4}" : '' |
|||
log.message = "Completed #{BOLD}#{http_code}#{CLEAR} #{m2}#{duration}" |
|||
if http_code / 100 == 2 |
|||
log.dimensions = AnsiDimensions.new(rails: TERMINUS_STRING, after: 1) |
|||
elsif http_code / 100 == 3 |
|||
@memory = '║' |
|||
log.dimensions = AnsiDimensions.new(rails: "╙#{@memory}╜") |
|||
elsif http_code / 100 == 4 |
|||
log.dimensions = AnsiDimensions.new(rails: '╙╨╜') |
|||
elsif http_code / 100 == 5 |
|||
log.dimensions = AnsiDimensions.new(rails: "╙#{draw_fatal}╜") |
|||
end |
|||
elsif log.message =~ /^(Processing|Parameters)/ |
|||
log.level = :debug |
|||
if log.message =~ /^Processing/ |
|||
log.message = two_captures_last_as_bold(log.message, /(^Processing by \w*#\w* as )(.*)/) |
|||
end |
|||
elsif log.message =~ /Redirected/ |
|||
log.level = :debug |
|||
@regex_redirected ||= build_regex_redirected # lazy building |
|||
log.message = two_captures_last_as_bold(log.message, @regex_redirected) |
|||
end |
|||
elsif log.exception |
|||
log.dimensions = AnsiDimensions.new( |
|||
rails: "╓#{draw_fatal(log.level.to_s.chr.upcase)}╖", |
|||
after: 1, |
|||
terminus: true |
|||
) |
|||
end |
|||
elsif log.name =~ /^(ActionView|ActiveRecord)::Base/ |
|||
log.level = :debug |
|||
log.message.lstrip! |
|||
if log.name == 'ActiveRecord::Base' |
|||
unbold!(log.message) |
|||
|
|||
if log.message.starts_with?('↳ ') |
|||
log.message = ansi_trace(log.message, '⇄') |
|||
else |
|||
sql_match = log.message.match(/\s+\[\[.*\]\]$/) |
|||
if sql_match |
|||
sql_command = sql_match.pre_match |
|||
sql_type, sql_entry = sql_command.split("\e[0m") |
|||
sql_color_entry = sql_entry.match(ANSI_REGEX).to_s |
|||
sql_args = JSON.parse(sql_match.to_s).map(&:last) |
|||
sql_args.each_with_index do |val, index| |
|||
sql_arg = val.inspect |
|||
sql_arg = "'#{val.gsub("'", "''")}'" if val.is_a?(String) |
|||
sql_entry.gsub!("$#{index + 1}", colorize(sql_arg, BOLD) + sql_color_entry) |
|||
end |
|||
log.message = [sql_type, sql_entry].join(CLEAR) |
|||
end |
|||
end |
|||
elsif log.name == 'ActionView::Base' |
|||
match = log.message.match(/^Rendered( layout| collection of|) (.*?\.erb)(.*)(\(Duration:.*\))/) |
|||
if match |
|||
m1, m2, m3, m4 = match.captures |
|||
duration = m4.match(/Duration: (\d+\.?\d*)ms/).match(1).to_f |
|||
duration = duration < RENDERED_VIEW_DURATION ? '' : m4.to_s |
|||
log.message = "⇤ Rendered#{m1} app/views/#{m2}#{m3}#{duration}" |
|||
end |
|||
end |
|||
|
|||
end |
|||
|
|||
log |
|||
end |
|||
|
|||
def draw_fatal(char = CHAR_FATAL) |
|||
BG_MAGENTA + BOLD + TEXT_WHITE + char + CLEAR |
|||
end |
|||
|
|||
def origin = colorize(centerize(log.name), TEXT_CYAN) |
|||
def build_prefix(char) = "#{origin} ╣#{colorize(char)}╠ " |
|||
def build_terminus = "#{origin} #{TERMINUS_STRING} " |
|||
def centerize(text) = text.truncate(CENTER_SIZE).center(CENTER_SIZE) |
|||
def colorize(text, tint = color) = "#{tint}#{text}#{CLEAR}" |
|||
|
|||
def stackisize(items) |
|||
return '' if items.empty? |
|||
|
|||
traces = items.map { |item| ansi_trace(item, '➟') } |
|||
"\n#{traces.join("\n")}" |
|||
end |
|||
|
|||
def build_dimensions(dimensions) |
|||
"#{origin} #{dimensions.rails} " |
|||
end |
|||
|
|||
def compute_useful_length |
|||
IO.console.winsize[1] - FOREMAN_PREFIX_LENGTH |
|||
rescue StandardError |
|||
FAILOVER_WRAP |
|||
end |
|||
|
|||
def unbold!(text) = text.gsub!(BOLD, '') |
|||
|
|||
def message |
|||
colorize(log.message) if log.message |
|||
end |
|||
|
|||
def payload |
|||
return unless log.payload |
|||
|
|||
log.payload.ai(indent: 2, object_id: false) |
|||
end |
|||
|
|||
def exception |
|||
return unless log.exception |
|||
|
|||
exc = log.exception |
|||
clazz = colorize("#{exc.class}\n", color_map[:fatal]) |
|||
message = colorize(exc.message.chomp(''), color_map[:error]) |
|||
backtrace = stackisize(Rails.backtrace_cleaner.clean(exc.backtrace)) |
|||
|
|||
"#{clazz}#{message}#{backtrace}" |
|||
end |
|||
|
|||
def level_char |
|||
case log.level |
|||
when :info, :debug then ' ' |
|||
else log.level.to_s.chr.upcase |
|||
end |
|||
end |
|||
|
|||
def wrap_level(length, *items) |
|||
prefix = log.dimensions ? build_dimensions(log.dimensions) : build_prefix(level_char) |
|||
continuation = build_prefix('┆') |
|||
|
|||
result = items.map do |item| |
|||
AnsiWrapper.wrap(item, length, prefix, continuation) |
|||
end |
|||
|
|||
if log.dimensions&.terminus |
|||
terminus = AnsiWrapper.wrap(' ', length, build_terminus) |
|||
result << terminus |
|||
end |
|||
|
|||
log.dimensions&.before&.times { result.unshift('') } |
|||
log.dimensions&.after&.times { result << '' } |
|||
result.compact.join("\n") |
|||
end |
|||
end |
|||
end |
@ -0,0 +1,82 @@ |
|||
require_relative 'ansi_colors' |
|||
|
|||
module Semantic |
|||
# AnsiWrapper cares about Ansi Colour Code \e[... |
|||
class AnsiWrapper |
|||
include AnsiColors |
|||
|
|||
TAB_TO_SPACES = 2 |
|||
|
|||
def self.wrap(text, length, prefix = '', continuation = prefix) |
|||
if visible_length(prefix) != visible_length(continuation) |
|||
raise "continuation <#{continuation.inspect}> should have the same length as prefix <#{prefix.inspect}>" |
|||
end |
|||
return unless text |
|||
|
|||
text = text.gsub("\t", ' ' * TAB_TO_SPACES) |
|||
|
|||
lines = split_text_to_lines(text, length - visible_length(prefix)) |
|||
lines = inject_continuation_and_ansi_colors_to_lines(lines, prefix, continuation) |
|||
lines.join("\n") |
|||
end |
|||
|
|||
private_class_method def self.inject_continuation_and_ansi_colors_to_lines(lines, prefix, continuation) |
|||
last_ansi = '' |
|||
lines.each_with_index.map do |line, index| |
|||
current = index.zero? ? prefix : continuation |
|||
current += last_ansi unless last_ansi.empty? || last_ansi == CLEAR |
|||
current += line |
|||
|
|||
last_ansi = scan_for_actual_ansi(line, last_ansi) |
|||
|
|||
current += CLEAR if last_ansi.empty? || last_ansi != CLEAR |
|||
current |
|||
end |
|||
end |
|||
|
|||
private_class_method def self.scan_for_actual_ansi(line, last_ansi) |
|||
line.scan(ANSI_REGEX).each do |match| |
|||
ansi_code = match.to_s |
|||
if ansi_code == CLEAR |
|||
last_ansi = CLEAR |
|||
else |
|||
last_ansi += ansi_code |
|||
end |
|||
end |
|||
last_ansi |
|||
end |
|||
|
|||
private_class_method def self.split_text_to_lines(text, length) |
|||
lines = text.split("\n") |
|||
sublines = lines.map do |line| |
|||
visible_length(line) > length ? visible_split(line, length) : [line] |
|||
end |
|||
sublines.flatten |
|||
end |
|||
|
|||
private_class_method def self.visible_length(line) |
|||
raise 'line should not contain carriage return character!' if line.match "\n" |
|||
|
|||
ansi_code_length = line.scan(ANSI_REGEX).map(&:length).sum |
|||
line.length - ansi_code_length |
|||
end |
|||
|
|||
# TODO: might be refactored with less complexity |
|||
private_class_method def self.visible_split(line, length, stack = '') # rubocop:disable Metrics/AbcSize,Metrics/MethodLength |
|||
before, ansi_code, after = line.partition(ANSI_REGEX) |
|||
stack_length = visible_length(stack) |
|||
visible_length = before.length + stack_length |
|||
if visible_length == length |
|||
["#{stack}#{before}#{ansi_code}"] + visible_split(after, length) |
|||
elsif visible_length > length |
|||
first_line = stack + before[0...length - stack_length] |
|||
tail = before[length - stack_length..] + ansi_code + after |
|||
[first_line] + visible_split(tail, length) |
|||
elsif ansi_code.length.positive? |
|||
visible_split(after, length, "#{stack}#{before}#{ansi_code}") |
|||
else |
|||
["#{stack}#{before}#{ansi_code}"] |
|||
end |
|||
end |
|||
end |
|||
end |
@ -0,0 +1,84 @@ |
|||
module Semantic |
|||
# My Custom colorized formatter |
|||
class BasicFormatter < SemanticLogger::Formatters::Color |
|||
ANSI_REVERSED_WARNING = "\e[0;30;43m".freeze |
|||
ANSI_REVERSED_ERROR = "\e[1;30;41m".freeze |
|||
ANSI_GRAY = "\e[90m".freeze |
|||
# Return the complete log level name in uppercase |
|||
|
|||
def initialize |
|||
super(time_format: '%H:%M:%S', |
|||
color_map: ColorMap.new( |
|||
debug: ANSI_GRAY, |
|||
info: SemanticLogger::AnsiColors::GREEN, |
|||
warn: SemanticLogger::AnsiColors::YELLOW |
|||
)) |
|||
@time_format = nil if File.exist?(File.join(Rails.root, 'Procfile.dev')) |
|||
end |
|||
|
|||
def time |
|||
"#{color}#{format_time(log.time)}#{color_map.clear}" if time_format |
|||
end |
|||
|
|||
def message |
|||
return unless log.message |
|||
|
|||
prefix = "#{color}--#{color_map.clear}" |
|||
|
|||
case log.level |
|||
when :info |
|||
message = log.message |
|||
message = message&.rstrip if log.name == 'Rails' && message.starts_with?('Completed') |
|||
if log.name == 'Rails' && message.starts_with?('Started') |
|||
message = message.split('for')[0] |
|||
puts '' if Rails.env.development? |
|||
end |
|||
if log.name == 'Rails' || log.name == 'ActionView::Base' |
|||
"#{prefix} #{ANSI_GRAY}#{message}#{color_map.clear}" |
|||
else |
|||
"#{prefix} #{SemanticLogger::AnsiColors::WHITE}#{message}#{color_map.clear}" |
|||
end |
|||
when :warn |
|||
"#{prefix} #{ANSI_REVERSED_WARNING}#{log.message}#{color_map.clear}" |
|||
when :error, :fatal |
|||
"#{prefix} #{ANSI_REVERSED_ERROR}#{log.message}#{color_map.clear}" |
|||
else |
|||
"#{prefix} #{color}#{log.message}#{color_map.clear}" |
|||
end |
|||
end |
|||
|
|||
def tags; end |
|||
|
|||
def process_info |
|||
fname = file_name_and_line |
|||
"#{color}[#{fname}]#{color_map.clear}" if fname |
|||
end |
|||
|
|||
def exception |
|||
return unless log.exception |
|||
|
|||
root_path = Rails.root.to_s |
|||
backtrace = log.exception.backtrace.select do |line| |
|||
line.starts_with?(root_path) |
|||
end |
|||
|
|||
if backtrace.count.positive? |
|||
"-- #{ANSI_REVERSED_ERROR}#{log.exception.class}#{color_map.clear} #{color}#{log.exception.message}#{color_map.clear}#{SemanticLogger::AnsiColors::WHITE}\n\t#{backtrace.join("\n\t")}#{color_map.clear}\n\n" |
|||
else |
|||
"-- #{ANSI_REVERSED_ERROR}#{log.exception.class}: #{log.exception.message}#{color_map.clear}\n\n" |
|||
end |
|||
end |
|||
|
|||
def call(log, logger) |
|||
self.color = color_map[log.level] |
|||
self.log = log |
|||
self.logger = logger |
|||
|
|||
if @time_format |
|||
[time, level, process_info, tags, named_tags, duration, name, message, payload, exception].compact.join(' ') |
|||
else |
|||
[tags, named_tags, duration, name, message, payload, exception].compact.join(' ') |
|||
end |
|||
end |
|||
end |
|||
end |
Write
Preview
Loading…
Cancel
Save
Reference in new issue