dry-logger 1.0.0.rc2 → 1.0.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -2
- data/lib/dry/logger/backends/core.rb +38 -0
- data/lib/dry/logger/backends/proxy.rb +65 -0
- data/lib/dry/logger/backends/stream.rb +6 -11
- data/lib/dry/logger/clock.rb +47 -0
- data/lib/dry/logger/constants.rb +55 -3
- data/lib/dry/logger/dispatcher.rb +81 -12
- data/lib/dry/logger/entry.rb +27 -33
- data/lib/dry/logger/formatters/colors.rb +84 -0
- data/lib/dry/logger/formatters/json.rb +27 -1
- data/lib/dry/logger/formatters/rack.rb +10 -2
- data/lib/dry/logger/formatters/string.rb +93 -26
- data/lib/dry/logger/formatters/structured.rb +16 -5
- data/lib/dry/logger/formatters/template.rb +92 -0
- data/lib/dry/logger/global.rb +123 -0
- data/lib/dry/logger/version.rb +1 -1
- data/lib/dry/logger.rb +45 -61
- metadata +10 -4
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Logger
|
5
|
+
module Formatters
|
6
|
+
# Shell colorizer
|
7
|
+
#
|
8
|
+
# This was ported from hanami-utils
|
9
|
+
#
|
10
|
+
# @since 1.0.0
|
11
|
+
# @api private
|
12
|
+
class Colors
|
13
|
+
# Unknown color code error
|
14
|
+
#
|
15
|
+
# @since 1.0.0
|
16
|
+
class UnknownColorCodeError < StandardError
|
17
|
+
def initialize(code)
|
18
|
+
super("Unknown color code: `#{code.inspect}'")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Escapes codes for terminals to output strings in colors
|
23
|
+
#
|
24
|
+
# @since 1.2.0
|
25
|
+
# @api private
|
26
|
+
COLORS = {black: 30,
|
27
|
+
red: 31,
|
28
|
+
green: 32,
|
29
|
+
yellow: 33,
|
30
|
+
blue: 34,
|
31
|
+
magenta: 35,
|
32
|
+
cyan: 36,
|
33
|
+
gray: 37}.freeze
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
def self.evaluate(input)
|
37
|
+
COLORS.keys.reduce(input.dup) { |output, color|
|
38
|
+
output.gsub!("<#{color}>", start(color))
|
39
|
+
output.gsub!("</#{color}>", stop)
|
40
|
+
output
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Colorizes output
|
45
|
+
# 8 colors available: black, red, green, yellow, blue, magenta, cyan, and gray
|
46
|
+
#
|
47
|
+
# @param input [#to_s] the string to colorize
|
48
|
+
# @param color [Symbol] the color
|
49
|
+
#
|
50
|
+
# @raise [UnknownColorError] if the color code is unknown
|
51
|
+
#
|
52
|
+
# @return [String] the colorized string
|
53
|
+
#
|
54
|
+
# @since 1.0.0
|
55
|
+
# @api private
|
56
|
+
def self.call(color, input)
|
57
|
+
"#{start(color)}#{input}#{stop}"
|
58
|
+
end
|
59
|
+
|
60
|
+
# @since 1.0.0
|
61
|
+
# @api private
|
62
|
+
def self.start(color)
|
63
|
+
"\e[#{self[color]}m"
|
64
|
+
end
|
65
|
+
|
66
|
+
# @since 1.0.0
|
67
|
+
# @api private
|
68
|
+
def self.stop
|
69
|
+
"\e[0m"
|
70
|
+
end
|
71
|
+
|
72
|
+
# Helper method to translate between color names and terminal escape codes
|
73
|
+
#
|
74
|
+
# @since 1.0.0
|
75
|
+
# @api private
|
76
|
+
#
|
77
|
+
# @raise [UnknownColorError] if the color code is unknown
|
78
|
+
def self.[](code)
|
79
|
+
COLORS.fetch(code) { raise UnknownColorCodeError, code }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "json"
|
4
|
+
|
5
|
+
require "dry/logger/constants"
|
4
6
|
require "dry/logger/formatters/structured"
|
5
7
|
|
6
8
|
module Dry
|
@@ -16,7 +18,31 @@ module Dry
|
|
16
18
|
# @since 0.1.0
|
17
19
|
# @api private
|
18
20
|
def format(entry)
|
19
|
-
|
21
|
+
hash = format_values(entry).compact
|
22
|
+
hash.update(hash.delete(:exception)) if entry.exception?
|
23
|
+
::JSON.dump(hash)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @since 0.1.0
|
27
|
+
# @api private
|
28
|
+
def format_severity(value)
|
29
|
+
value.upcase
|
30
|
+
end
|
31
|
+
|
32
|
+
# @since 0.1.0
|
33
|
+
# @api private
|
34
|
+
def format_exception(value)
|
35
|
+
{
|
36
|
+
exception: value.class,
|
37
|
+
message: value.message,
|
38
|
+
backtrace: value.backtrace || EMPTY_ARRAY
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
# @since 0.1.0
|
43
|
+
# @api private
|
44
|
+
def format_time(value)
|
45
|
+
value.getutc.iso8601
|
20
46
|
end
|
21
47
|
end
|
22
48
|
end
|
@@ -12,10 +12,18 @@ module Dry
|
|
12
12
|
#
|
13
13
|
# @see String
|
14
14
|
class Rack < String
|
15
|
+
# @see String#initialize
|
15
16
|
# @since 1.0.0
|
16
17
|
# @api private
|
17
|
-
def
|
18
|
-
|
18
|
+
def initialize(**options)
|
19
|
+
super
|
20
|
+
@template = Template[Logger.templates[:rack]]
|
21
|
+
end
|
22
|
+
|
23
|
+
# @api 1.0.0
|
24
|
+
# @api private
|
25
|
+
def format_params(value)
|
26
|
+
return value unless value.empty?
|
19
27
|
end
|
20
28
|
end
|
21
29
|
end
|
@@ -1,6 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
require_relative "template"
|
6
|
+
require_relative "structured"
|
4
7
|
|
5
8
|
module Dry
|
6
9
|
module Logger
|
@@ -12,10 +15,6 @@ module Dry
|
|
12
15
|
# @since 1.0.0
|
13
16
|
# @api public
|
14
17
|
class String < Structured
|
15
|
-
# @since 1.0.0
|
16
|
-
# @api private
|
17
|
-
SEPARATOR = " "
|
18
|
-
|
19
18
|
# @since 1.0.0
|
20
19
|
# @api private
|
21
20
|
HASH_SEPARATOR = ","
|
@@ -24,9 +23,16 @@ module Dry
|
|
24
23
|
# @api private
|
25
24
|
EXCEPTION_SEPARATOR = ": "
|
26
25
|
|
27
|
-
# @since 1.
|
26
|
+
# @since 1.2.0
|
28
27
|
# @api private
|
29
|
-
|
28
|
+
DEFAULT_SEVERITY_COLORS = {
|
29
|
+
DEBUG => :cyan,
|
30
|
+
INFO => :magenta,
|
31
|
+
WARN => :yellow,
|
32
|
+
ERROR => :red,
|
33
|
+
FATAL => :red,
|
34
|
+
UNKNOWN => :blue
|
35
|
+
}.freeze
|
30
36
|
|
31
37
|
# @since 1.0.0
|
32
38
|
# @api private
|
@@ -34,47 +40,108 @@ module Dry
|
|
34
40
|
|
35
41
|
# @since 1.0.0
|
36
42
|
# @api private
|
37
|
-
def initialize(template:
|
43
|
+
def initialize(template: Logger.templates[:default], **options)
|
38
44
|
super(**options)
|
39
|
-
@template = template
|
45
|
+
@template = Template[template]
|
46
|
+
end
|
47
|
+
|
48
|
+
# @since 1.0.0
|
49
|
+
# @api private
|
50
|
+
def colorize?
|
51
|
+
options[:colorize].equal?(true)
|
40
52
|
end
|
41
53
|
|
42
54
|
private
|
43
55
|
|
44
56
|
# @since 1.0.0
|
45
57
|
# @api private
|
46
|
-
def
|
47
|
-
|
58
|
+
def format_severity(value)
|
59
|
+
output = value.upcase
|
60
|
+
|
61
|
+
if colorize?
|
62
|
+
Colors.call(severity_colors[LEVELS[value]], output)
|
63
|
+
else
|
64
|
+
output
|
65
|
+
end
|
48
66
|
end
|
49
67
|
|
50
68
|
# @since 1.0.0
|
51
69
|
# @api private
|
52
|
-
def
|
70
|
+
def format(entry)
|
53
71
|
if entry.exception?
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
else
|
59
|
-
"#{entry.message}#{SEPARATOR}#{format_payload(entry)}"
|
60
|
-
end
|
72
|
+
head = template % template_data(entry, exclude: %i[exception])
|
73
|
+
tail = format_exception(entry.exception)
|
74
|
+
|
75
|
+
"#{head}#{NEW_LINE}#{TAB}#{tail}"
|
61
76
|
else
|
62
|
-
|
77
|
+
template % template_data(entry)
|
63
78
|
end
|
64
79
|
end
|
65
80
|
|
66
81
|
# @since 1.0.0
|
67
82
|
# @api private
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
|
83
|
+
def format_tags(value)
|
84
|
+
Array(value)
|
85
|
+
.map { |tag|
|
86
|
+
case tag
|
87
|
+
when Hash then format_payload(tag)
|
88
|
+
else
|
89
|
+
tag.to_s
|
90
|
+
end
|
91
|
+
}
|
92
|
+
.join(SEPARATOR)
|
93
|
+
end
|
94
|
+
|
95
|
+
# @since 1.0.0
|
96
|
+
# @api private
|
97
|
+
def format_exception(value)
|
98
|
+
[
|
99
|
+
"#{value.message} (#{value.class})",
|
100
|
+
format_backtrace(value.backtrace || EMPTY_BACKTRACE)
|
101
|
+
].join(NEW_LINE)
|
102
|
+
end
|
103
|
+
|
104
|
+
# @since 1.0.0
|
105
|
+
# @api private
|
106
|
+
def format_payload(payload)
|
107
|
+
payload.map { |key, value| "#{key}=#{value.inspect}" }.join(SEPARATOR)
|
108
|
+
end
|
109
|
+
|
110
|
+
# @since 1.0.0
|
111
|
+
# @api private
|
112
|
+
def format_backtrace(value)
|
113
|
+
value.map { |line| "#{TAB}#{line}" }.join(NEW_LINE)
|
114
|
+
end
|
115
|
+
|
116
|
+
# @since 1.0.0
|
117
|
+
# @api private
|
118
|
+
def template_data(entry, exclude: EMPTY_ARRAY)
|
119
|
+
data = format_values(entry)
|
120
|
+
payload = data.except(:message, *entry.meta.keys, *template.tokens, *exclude)
|
121
|
+
data[:payload] = format_payload(payload)
|
122
|
+
|
123
|
+
if template.include?(:tags)
|
124
|
+
data[:tags] = format_tags(entry.tags)
|
125
|
+
end
|
126
|
+
|
127
|
+
if data[:message]
|
128
|
+
data.except(*payload.keys)
|
129
|
+
elsif template.include?(:message)
|
130
|
+
data[:message] = data.delete(:payload)
|
131
|
+
data[:payload] = nil
|
132
|
+
data
|
133
|
+
else
|
134
|
+
data
|
135
|
+
end
|
72
136
|
end
|
73
137
|
|
74
138
|
# @since 1.0.0
|
75
139
|
# @api private
|
76
|
-
def
|
77
|
-
|
140
|
+
def severity_colors
|
141
|
+
@severity_colors ||= DEFAULT_SEVERITY_COLORS.merge(
|
142
|
+
(options[:severity_colors] || EMPTY_HASH)
|
143
|
+
.to_h { |key, value| [LEVELS[key.to_s], value] }
|
144
|
+
)
|
78
145
|
end
|
79
146
|
end
|
80
147
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "logger"
|
4
|
+
|
5
|
+
require "dry/logger/constants"
|
4
6
|
require "dry/logger/filter"
|
5
7
|
|
6
8
|
module Dry
|
@@ -8,6 +10,8 @@ module Dry
|
|
8
10
|
module Formatters
|
9
11
|
# Default structured formatter which receives {Logger::Entry} from the backends.
|
10
12
|
#
|
13
|
+
# This class can be used as the base class for your custom formatters.
|
14
|
+
#
|
11
15
|
# @see http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/Logger/Formatter.html
|
12
16
|
#
|
13
17
|
# @since 1.0.0
|
@@ -21,10 +25,6 @@ module Dry
|
|
21
25
|
# @api private
|
22
26
|
NOOP_FILTER = -> message { message }
|
23
27
|
|
24
|
-
# @since 1.0.0
|
25
|
-
# @api private
|
26
|
-
NEW_LINE = $/ # rubocop:disable Style/SpecialGlobalVars
|
27
|
-
|
28
28
|
# @since 1.0.0
|
29
29
|
# @api private
|
30
30
|
attr_reader :filter
|
@@ -52,7 +52,7 @@ module Dry
|
|
52
52
|
# @return [String]
|
53
53
|
# @api public
|
54
54
|
def call(_severity, _time, _progname, entry)
|
55
|
-
format(entry.filter(filter))
|
55
|
+
format(entry.filter(filter)) + NEW_LINE
|
56
56
|
end
|
57
57
|
|
58
58
|
# Format entry into a loggable object
|
@@ -63,7 +63,18 @@ module Dry
|
|
63
63
|
# @return [Entry]
|
64
64
|
# @api public
|
65
65
|
def format(entry)
|
66
|
+
format_values(entry)
|
67
|
+
end
|
68
|
+
|
69
|
+
# @since 1.0.0
|
70
|
+
# @api private
|
71
|
+
def format_values(entry)
|
66
72
|
entry
|
73
|
+
.to_h
|
74
|
+
.map { |key, value|
|
75
|
+
[key, respond_to?(meth = "format_#{key}", true) ? __send__(meth, value) : value]
|
76
|
+
}
|
77
|
+
.to_h
|
67
78
|
end
|
68
79
|
end
|
69
80
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
require "dry/logger/constants"
|
5
|
+
require_relative "colors"
|
6
|
+
|
7
|
+
module Dry
|
8
|
+
module Logger
|
9
|
+
module Formatters
|
10
|
+
# Basic string formatter.
|
11
|
+
#
|
12
|
+
# This formatter returns log entries in key=value format.
|
13
|
+
#
|
14
|
+
# @since 1.0.0
|
15
|
+
# @api public
|
16
|
+
class Template
|
17
|
+
# @since 1.0.0
|
18
|
+
# @api private
|
19
|
+
TOKEN_REGEXP = /%<(\w*)>s/.freeze
|
20
|
+
|
21
|
+
# @since 1.0.0
|
22
|
+
# @api private
|
23
|
+
MESSAGE_TOKEN = "%<message>s"
|
24
|
+
|
25
|
+
# @since 1.0.0
|
26
|
+
# @api private
|
27
|
+
attr_reader :value
|
28
|
+
|
29
|
+
# @since 1.0.0
|
30
|
+
# @api private
|
31
|
+
attr_reader :tokens
|
32
|
+
|
33
|
+
# @since 1.0.0
|
34
|
+
# @api private
|
35
|
+
def self.[](value)
|
36
|
+
cache.fetch(value) {
|
37
|
+
cache[value] = (colorized?(value) ? Template::Colorized : Template).new(value)
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
# @since 1.0.0
|
42
|
+
# @api private
|
43
|
+
private_class_method def self.colorized?(value)
|
44
|
+
Colors::COLORS.keys.any? { |color| value.include?("<#{color}>") }
|
45
|
+
end
|
46
|
+
|
47
|
+
# @since 1.0.0
|
48
|
+
# @api private
|
49
|
+
private_class_method def self.cache
|
50
|
+
@cache ||= {}
|
51
|
+
end
|
52
|
+
|
53
|
+
# @since 1.0.0
|
54
|
+
# @api private
|
55
|
+
class Colorized < Template
|
56
|
+
# @since 1.0.0
|
57
|
+
# @api private
|
58
|
+
def initialize(value)
|
59
|
+
super(Colors.evaluate(value))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# @since 1.0.0
|
64
|
+
# @api private
|
65
|
+
def initialize(value)
|
66
|
+
@value = value
|
67
|
+
@tokens = value.scan(TOKEN_REGEXP).flatten(1).map(&:to_sym).to_set
|
68
|
+
end
|
69
|
+
|
70
|
+
# @since 1.0.0
|
71
|
+
# @api private
|
72
|
+
def %(tokens)
|
73
|
+
output = value % tokens
|
74
|
+
output.strip!
|
75
|
+
output.split(NEW_LINE).map(&:rstrip).join(NEW_LINE)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @since 1.0.0
|
79
|
+
# @api private
|
80
|
+
def colorize(color, input)
|
81
|
+
"\e[#{Colors[color.to_sym]}m#{input}\e[0m"
|
82
|
+
end
|
83
|
+
|
84
|
+
# @since 1.0.0
|
85
|
+
# @api private
|
86
|
+
def include?(token)
|
87
|
+
tokens.include?(token)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "stringio"
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Logger
|
7
|
+
# Global setup methods
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
module Global
|
11
|
+
# Register a new formatter
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# class MyFormatter < Dry::Logger::Formatters::Structured
|
15
|
+
# def format_message(value)
|
16
|
+
# "WOAH: #{message}"
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# def format_time(value)
|
20
|
+
# Time.now.strftime("%Y-%m-%d %H:%M:%S")
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# Dry::Logger.register_formatter(:my_formatter, MyFormatter)
|
25
|
+
#
|
26
|
+
# logger = Dry.Logger(:app, formatter: :my_formatter, template: :details)
|
27
|
+
#
|
28
|
+
# logger.info "Hello World"
|
29
|
+
# # [test] [INFO] [2022-11-15 10:06:29] WOAH: Hello World
|
30
|
+
#
|
31
|
+
# @since 1.0.0
|
32
|
+
# @return [Hash]
|
33
|
+
# @api public
|
34
|
+
def register_formatter(name, formatter)
|
35
|
+
formatters[name] = formatter
|
36
|
+
formatters
|
37
|
+
end
|
38
|
+
|
39
|
+
# Register a new template
|
40
|
+
#
|
41
|
+
# @example basic template
|
42
|
+
# Dry::Logger.register_template(:request, "[%<severity>s] %<verb>s %<path>s")
|
43
|
+
#
|
44
|
+
# logger = Dry.Logger(:my_app, template: :request)
|
45
|
+
#
|
46
|
+
# logger.info(verb: "GET", path: "/users")
|
47
|
+
# # [INFO] GET /users
|
48
|
+
#
|
49
|
+
# @example template with colors
|
50
|
+
# Dry::Logger.register_template(
|
51
|
+
# :request, "[%<severity>s] <green>%<verb>s</green> <blue>%<path>s</blue>"
|
52
|
+
# )
|
53
|
+
#
|
54
|
+
# @since 1.0.0
|
55
|
+
# @return [Hash]
|
56
|
+
# @api public
|
57
|
+
def register_template(name, template)
|
58
|
+
templates[name] = template
|
59
|
+
templates
|
60
|
+
end
|
61
|
+
|
62
|
+
# Build a logging backend instance
|
63
|
+
#
|
64
|
+
# @since 1.0.0
|
65
|
+
# @return [Backends::Stream]
|
66
|
+
# @api private
|
67
|
+
def new(stream: $stdout, **options)
|
68
|
+
backend =
|
69
|
+
case stream
|
70
|
+
when IO, StringIO then Backends::IO
|
71
|
+
when String, Pathname then Backends::File
|
72
|
+
else
|
73
|
+
raise ArgumentError, "unsupported stream type #{stream.class}"
|
74
|
+
end
|
75
|
+
|
76
|
+
formatter_spec = options[:formatter]
|
77
|
+
template_spec = options[:template]
|
78
|
+
|
79
|
+
template =
|
80
|
+
case template_spec
|
81
|
+
when Symbol then templates.fetch(template_spec)
|
82
|
+
when String then template_spec
|
83
|
+
when nil then templates[:default]
|
84
|
+
else
|
85
|
+
raise ArgumentError,
|
86
|
+
":template option must be a Symbol or a String (`#{template_spec}` given)"
|
87
|
+
end
|
88
|
+
|
89
|
+
formatter_options = {**options, template: template}
|
90
|
+
|
91
|
+
formatter =
|
92
|
+
case formatter_spec
|
93
|
+
when Symbol then formatters.fetch(formatter_spec).new(**formatter_options)
|
94
|
+
when Class then formatter_spec.new(**formatter_options)
|
95
|
+
when nil then formatters[:string].new(**formatter_options)
|
96
|
+
when ::Logger::Formatter then formatter_spec
|
97
|
+
else
|
98
|
+
raise ArgumentError, "Unsupported formatter option #{formatter_spec.inspect}"
|
99
|
+
end
|
100
|
+
|
101
|
+
backend_options = options.select { |key, _| BACKEND_OPT_KEYS.include?(key) }
|
102
|
+
|
103
|
+
backend.new(stream: stream, **backend_options, formatter: formatter)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Internal formatters registry
|
107
|
+
#
|
108
|
+
# @since 1.0.0
|
109
|
+
# @api private
|
110
|
+
def formatters
|
111
|
+
@formatters ||= {}
|
112
|
+
end
|
113
|
+
|
114
|
+
# Internal templates registry
|
115
|
+
#
|
116
|
+
# @since 1.0.0
|
117
|
+
# @api private
|
118
|
+
def templates
|
119
|
+
@templates ||= {}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|