tty-logger 0.0.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Logger
5
+ class DataFilter
6
+ FILTERED = "[FILTERED]"
7
+ DOT = "."
8
+
9
+ attr_reader :filters, :compiled_filters, :mask
10
+
11
+ # Create a data filter instance with filters.
12
+ #
13
+ # @example
14
+ # TTY::Logger::DataFilter.new(%w[foo], mask: "<SECRET>")
15
+ #
16
+ # @param [String] mask
17
+ # the mask to replace object with. Defaults to `"[FILTERED]"`
18
+ #
19
+ # @api private
20
+ def initialize(filters = [], mask: nil)
21
+ @mask = mask || FILTERED
22
+ @filters = filters
23
+ @compiled_filters = compile(filters)
24
+ end
25
+
26
+ # Filter object for keys matching provided filters.
27
+ #
28
+ # @example
29
+ # data_filter = TTY::Logger::DataFilter.new(%w[foo])
30
+ # data_filter.filter({"foo" => "bar"})
31
+ # # => {"foo" => "[FILTERED]"}
32
+ #
33
+ # @param [Object] obj
34
+ # the object to filter
35
+ #
36
+ # @api public
37
+ def filter(obj)
38
+ return obj if filters.empty?
39
+
40
+ obj.each_with_object({}) do |(k, v), acc|
41
+ acc[k] = filter_val(k, v)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def compile(filters)
48
+ compiled = {
49
+ regexps: [],
50
+ nested_regexps: [],
51
+ blocks: []
52
+ }
53
+ strings = []
54
+ nested_strings = []
55
+
56
+ filters.each do |filter|
57
+ case filter
58
+ when Proc
59
+ compiled[:blocks] << filter
60
+ when Regexp
61
+ if filter.to_s.include?(DOT)
62
+ compiled[:nested_regexps] << filter
63
+ else
64
+ compiled[:regexps] << filter
65
+ end
66
+ else
67
+ exp = Regexp.escape(filter)
68
+ if exp.include?(DOT)
69
+ nested_strings << exp
70
+ else
71
+ strings << exp
72
+ end
73
+ end
74
+ end
75
+
76
+ if !strings.empty?
77
+ compiled[:regexps] << /^(#{strings.join("|")})$/
78
+ end
79
+
80
+ if !nested_strings.empty?
81
+ compiled[:nested_regexps] << /^(#{nested_strings.join("|")})$/
82
+ end
83
+
84
+ compiled
85
+ end
86
+
87
+ def filter_val(key, val, composite = [])
88
+ return mask if filtered?(key, composite)
89
+
90
+ case val
91
+ when Hash then filter_obj(key, val, composite << key)
92
+ when Array then filter_arr(key, val, composite)
93
+ else val
94
+ end
95
+ end
96
+
97
+ def filtered?(key, composite)
98
+ composite_key = composite + [key]
99
+ joined_key = composite_key.join(DOT)
100
+ @compiled_filters[:regexps].any? { |reg| !!reg.match(key.to_s) } ||
101
+ @compiled_filters[:nested_regexps].any? { |reg| !!reg.match(joined_key) } ||
102
+ @compiled_filters[:blocks].any? { |block| block.(composite_key.dup) }
103
+ end
104
+
105
+ def filter_obj(_key, obj, composite)
106
+ obj.each_with_object({}) do |(k, v), acc|
107
+ acc[k] = filter_val(k, v, composite)
108
+ end
109
+ end
110
+
111
+ def filter_arr(key, obj, composite)
112
+ obj.reduce([]) do |acc, v|
113
+ acc << filter_val(key, v, composite)
114
+ end
115
+ end
116
+ end # DataFilter
117
+ end # Logger
118
+ end # TTY
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Logger
5
+ class Event
6
+ attr_reader :message
7
+
8
+ attr_reader :fields
9
+
10
+ attr_reader :metadata
11
+
12
+ attr_reader :backtrace
13
+
14
+ def initialize(message, fields = {}, metadata = {})
15
+ @message = message
16
+ @fields = fields
17
+ @metadata = metadata
18
+ @backtrace = []
19
+
20
+ evaluate_message
21
+ end
22
+
23
+ private
24
+
25
+ # Extract backtrace information if message contains exception
26
+ #
27
+ # @api private
28
+ def evaluate_message
29
+ @message.each do |msg|
30
+ case msg
31
+ when Exception
32
+ @backtrace = msg.backtrace if msg.backtrace
33
+ else
34
+ msg
35
+ end
36
+ end
37
+ end
38
+ end # Event
39
+ end # Logger
40
+ end # TTY
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module TTY
6
+ class Logger
7
+ module Formatters
8
+ # Format data suitable for data exchange
9
+ class JSON
10
+ ELLIPSIS = "..."
11
+
12
+ # Dump data into a JSON formatted string
13
+ #
14
+ # @param [Hash] obj
15
+ # the object to serialize as JSON
16
+ #
17
+ # @return [String]
18
+ #
19
+ # @api public
20
+ def dump(obj, max_bytes: 2**12, max_depth: 3)
21
+ bytesize = 0
22
+
23
+ hash = obj.each_with_object({}) do |(k, v), acc|
24
+ str = (k.to_json + v.to_json)
25
+ items = acc.keys.size - 1
26
+
27
+ if bytesize + str.bytesize + items + ELLIPSIS.bytesize > max_bytes
28
+ acc[k] = ELLIPSIS
29
+ break acc
30
+ else
31
+ bytesize += str.bytesize
32
+ acc[k] = dump_val(v, max_depth)
33
+ end
34
+ end
35
+ ::JSON.generate(hash)
36
+ end
37
+
38
+ private
39
+
40
+ def dump_val(val, depth)
41
+ case val
42
+ when Hash then enc_obj(val, depth - 1)
43
+ when Array then enc_arr(val, depth - 1)
44
+ else
45
+ val
46
+ end
47
+ end
48
+
49
+ def enc_obj(obj, depth)
50
+ return ELLIPSIS if depth.zero?
51
+
52
+ obj.each_with_object({}) { |(k, v), acc| acc[k] = dump_val(v, depth) }
53
+ end
54
+
55
+ def enc_arr(obj, depth)
56
+ return ELLIPSIS if depth.zero?
57
+
58
+ obj.each_with_object([]) { |v, acc| acc << dump_val(v, depth) }
59
+ end
60
+ end # JSON
61
+ end # Formatters
62
+ end # Logger
63
+ end # TTY
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Logger
5
+ module Formatters
6
+ # Format data suitable for text reading
7
+ class Text
8
+ SPACE = " "
9
+ LPAREN = "("
10
+ RPAREN = ")"
11
+ LBRACE = "{"
12
+ RBRACE = "}"
13
+ LBRACKET = "["
14
+ RBRACKET = "]"
15
+ ELLIPSIS = "..."
16
+ LITERAL_TRUE = "true"
17
+ LITERAL_FALSE = "false"
18
+ LITERAL_NIL = "nil"
19
+ SINGLE_QUOTE_REGEX = /'/.freeze
20
+ ESCAPE_DOUBLE_QUOTE = "\""
21
+ ESCAPE_STR_REGEX = /[ ="|{}()\[\]^$+*?.-]/.freeze
22
+ NUM_REGEX = /^-?\d*(?:\.\d+)?\d+$/.freeze
23
+
24
+ # Dump data in a single formatted line
25
+ #
26
+ # @param [Hash] obj
27
+ # the object to serialize as text
28
+ #
29
+ # @return [String]
30
+ #
31
+ # @api public
32
+ def dump(obj, max_bytes: 2**12, max_depth: 3)
33
+ bytesize = 0
34
+
35
+ line = obj.each_with_object([]) do |(k, v), acc|
36
+ str = "#{dump_key(k)}=#{dump_val(v, max_depth)}"
37
+ items = acc.size - 1
38
+
39
+ if bytesize + str.bytesize + items > max_bytes
40
+ if bytesize + items +
41
+ (acc[-1].bytesize - ELLIPSIS.bytesize) > max_bytes
42
+ acc.pop
43
+ end
44
+ acc << ELLIPSIS
45
+ break acc
46
+ else
47
+ bytesize += str.bytesize
48
+ acc << str
49
+ end
50
+ end
51
+ line.join(SPACE)
52
+ end
53
+
54
+ private
55
+
56
+ def dump_key(key)
57
+ key = key.to_s
58
+ case key
59
+ when SINGLE_QUOTE_REGEX
60
+ key.inspect
61
+ when ESCAPE_STR_REGEX
62
+ ESCAPE_DOUBLE_QUOTE + key.inspect[1..-2] + ESCAPE_DOUBLE_QUOTE
63
+ else
64
+ key
65
+ end
66
+ end
67
+
68
+ def dump_val(val, depth)
69
+ case val
70
+ when Hash then enc_obj(val, depth - 1)
71
+ when Array then enc_arr(val, depth - 1)
72
+ when String, Symbol then enc_str(val)
73
+ when Complex then enc_cpx(val)
74
+ when Float then enc_float(val)
75
+ when Numeric then enc_num(val)
76
+ when Time then enc_time(val)
77
+ when TrueClass then LITERAL_TRUE
78
+ when FalseClass then LITERAL_FALSE
79
+ when NilClass then LITERAL_NIL
80
+ else
81
+ val
82
+ end
83
+ end
84
+
85
+ def enc_obj(obj, depth)
86
+ return LBRACE + ELLIPSIS + RBRACE if depth.zero?
87
+
88
+ LBRACE +
89
+ obj.map { |k, v| "#{dump_key(k)}=#{dump_val(v, depth)}" }
90
+ .join(SPACE) + RBRACE
91
+ end
92
+
93
+ def enc_arr(array, depth)
94
+ return LBRACKET + ELLIPSIS + RBRACKET if depth.zero?
95
+
96
+ LBRACKET + array.map { |v| dump_val(v, depth) }.join(SPACE) + RBRACKET
97
+ end
98
+
99
+ def enc_cpx(val)
100
+ LPAREN + val.to_s + RPAREN
101
+ end
102
+
103
+ def enc_float(val)
104
+ ("%f" % val).sub(/0*?$/, "")
105
+ end
106
+
107
+ def enc_num(val)
108
+ val
109
+ end
110
+
111
+ def enc_str(str)
112
+ str = str.to_s
113
+ case str
114
+ when SINGLE_QUOTE_REGEX
115
+ str.inspect
116
+ when ESCAPE_STR_REGEX, LITERAL_TRUE, LITERAL_FALSE,
117
+ LITERAL_NIL, NUM_REGEX
118
+ ESCAPE_DOUBLE_QUOTE + str.inspect[1..-2] + ESCAPE_DOUBLE_QUOTE
119
+ else
120
+ str
121
+ end
122
+ end
123
+
124
+ def enc_time(time)
125
+ time.strftime("%FT%T%:z")
126
+ end
127
+ end # Text
128
+ end # Formatters
129
+ end # Logger
130
+ end # TTY
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Logger
5
+ module Handlers
6
+ module Base
7
+ # Change current log level for the duration of the block
8
+ #
9
+ # @param [String] tmp_level
10
+ # the temporary log level
11
+ #
12
+ # @api public
13
+ def log_at(tmp_level)
14
+ old_level, @level = level, tmp_level
15
+ yield
16
+ ensure
17
+ @level = old_level
18
+ end
19
+
20
+ # Coerce formatter name into constant
21
+ #
22
+ # @api private
23
+ def coerce_formatter(name)
24
+ case name
25
+ when String, Symbol
26
+ const_name = if Formatters.const_defined?(name.upcase)
27
+ name.upcase
28
+ else
29
+ name.capitalize
30
+ end
31
+ Formatters.const_get(const_name)
32
+ when Class
33
+ name
34
+ else
35
+ raise_formatter_error(name)
36
+ end
37
+ rescue NameError
38
+ raise_formatter_error(name)
39
+ end
40
+
41
+ # Raise error when unknown formatter name
42
+ #
43
+ # @api private
44
+ def raise_formatter_error(name)
45
+ raise Error, "Unrecognized formatter name '#{name.inspect}'"
46
+ end
47
+
48
+ # Metadata for the log event
49
+ #
50
+ # @return [Array[Symbol]]
51
+ #
52
+ # @api private
53
+ def metadata
54
+ if config.metadata.include?(:all)
55
+ [:pid, :date, :time, :file]
56
+ else
57
+ config.metadata
58
+ end
59
+ end
60
+
61
+ # Format path from event metadata
62
+ #
63
+ # @return [String]
64
+ #
65
+ # @api private
66
+ def format_filepath(event)
67
+ "%s:%d:in`%s`" % [event.metadata[:path], event.metadata[:lineno],
68
+ event.metadata[:method]]
69
+ end
70
+ end # Base
71
+ end # Handlers
72
+ end # Logger
73
+ end # TTY
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ require_relative "base"
6
+
7
+ module TTY
8
+ class Logger
9
+ module Handlers
10
+ class Console
11
+ include Base
12
+
13
+ ARROW = "›"
14
+
15
+ STYLES = {
16
+ debug: {
17
+ label: "debug",
18
+ symbol: "•",
19
+ color: :cyan,
20
+ levelpad: 2
21
+ },
22
+ info: {
23
+ label: "info",
24
+ symbol: "ℹ",
25
+ color: :green,
26
+ levelpad: 3
27
+ },
28
+ warn: {
29
+ label: "warning",
30
+ symbol: "⚠",
31
+ color: :yellow,
32
+ levelpad: 0
33
+ },
34
+ error: {
35
+ label: "error",
36
+ symbol: "⨯",
37
+ color: :red,
38
+ levelpad: 2
39
+ },
40
+ fatal: {
41
+ label: "fatal",
42
+ symbol: "!",
43
+ color: :red,
44
+ levelpad: 2
45
+ },
46
+ success: {
47
+ label: "success",
48
+ symbol: "✔",
49
+ color: :green,
50
+ levelpad: 0
51
+ },
52
+ wait: {
53
+ label: "waiting",
54
+ symbol: "…",
55
+ color: :cyan,
56
+ levelpad: 0
57
+ }
58
+ }.freeze
59
+
60
+ TEXT_REGEXP = /([{}()\[\]])?(["']?)(\S+?)(["']?=)/.freeze
61
+ JSON_REGEXP = /\"([^,]+?)\"(?=:)/.freeze
62
+
63
+ COLOR_PATTERNS = {
64
+ text: [TEXT_REGEXP, ->(c) { "\\1\\2" + c.("\\3") + "\\4" }],
65
+ json: [JSON_REGEXP, ->(c) { "\"" + c.("\\1") + "\"" }]
66
+ }.freeze
67
+
68
+ # The output stream
69
+ # @api private
70
+ attr_reader :output
71
+
72
+ # The configuration options
73
+ # @api private
74
+ attr_reader :config
75
+
76
+ # The logging level
77
+ # @api private
78
+ attr_reader :level
79
+
80
+ # The format for the message
81
+ # @api private
82
+ attr_reader :message_format
83
+
84
+ def initialize(output: $stderr, formatter: nil, config: nil, level: nil,
85
+ styles: {}, message_format: "%-25s")
86
+ @output = Array[output].flatten
87
+ @formatter = coerce_formatter(formatter || config.formatter).new
88
+ @formatter_name = @formatter.class.name.split("::").last.downcase
89
+ @color_pattern = COLOR_PATTERNS[@formatter_name.to_sym]
90
+ @config = config
91
+ @styles = styles
92
+ @level = level || @config.level
93
+ @mutex = Mutex.new
94
+ @pastel = Pastel.new
95
+ @message_format = message_format
96
+ end
97
+
98
+ # Handle log event output in format
99
+ #
100
+ # @param [Event] event
101
+ # the current event logged
102
+ #
103
+ # @api public
104
+ def call(event)
105
+ @mutex.lock
106
+
107
+ style = configure_styles(event)
108
+ color = configure_color(style)
109
+
110
+ fmt = []
111
+ metadata.each do |meta|
112
+ case meta
113
+ when :date
114
+ fmt << @pastel.white("[" + event.metadata[:time].
115
+ strftime(config.date_format) + "]")
116
+ when :time
117
+ fmt << @pastel.white("[" + event.metadata[:time].
118
+ strftime(config.time_format) + "]")
119
+ when :file
120
+ fmt << @pastel.white("[#{format_filepath(event)}]")
121
+ when :pid
122
+ fmt << @pastel.white("[%d]" % event.metadata[:pid])
123
+ else
124
+ raise "Unknown metadata `#{meta}`"
125
+ end
126
+ end
127
+ fmt << ARROW unless config.metadata.empty?
128
+ unless style.empty?
129
+ fmt << color.(style[:symbol])
130
+ fmt << color.(style[:label]) + (" " * style[:levelpad])
131
+ end
132
+ fmt << message_format % event.message.join(" ")
133
+ unless event.fields.empty?
134
+ pattern, replacement = *@color_pattern
135
+ fmt << @formatter.dump(event.fields, max_bytes: config.max_bytes,
136
+ max_depth: config.max_depth)
137
+ .gsub(pattern, replacement.(color))
138
+ end
139
+ unless event.backtrace.empty?
140
+ fmt << "\n" + format_backtrace(event)
141
+ end
142
+
143
+ output.each { |out| out.puts fmt.join(" ") }
144
+ ensure
145
+ @mutex.unlock
146
+ end
147
+
148
+ private
149
+
150
+ def format_backtrace(event)
151
+ indent = " " * 4
152
+ event.backtrace.map do |bktrace|
153
+ indent + bktrace.to_s
154
+ end.join("\n")
155
+ end
156
+
157
+ # Merge default styles with custom style overrides
158
+ #
159
+ # @return [Hash[String]]
160
+ # the style matching log type
161
+ #
162
+ # @api private
163
+ def configure_styles(event)
164
+ return {} if event.metadata[:name].nil?
165
+
166
+ STYLES.fetch(event.metadata[:name].to_sym, {})
167
+ .dup
168
+ .merge!(@styles[event.metadata[:name].to_sym] || {})
169
+ end
170
+
171
+ def configure_color(style)
172
+ color = style.fetch(:color) { :cyan }
173
+ @pastel.send(color).detach
174
+ end
175
+ end # Console
176
+ end # Handlers
177
+ end # Logger
178
+ end # TTY