tty-logger 0.0.0 → 0.1.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,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "handlers/console"
4
+
5
+ module TTY
6
+ class Logger
7
+ class Config
8
+ # The format used for date display
9
+ attr_accessor :date_format
10
+
11
+ # The format used for time display
12
+ attr_accessor :time_format
13
+
14
+ # The format used for displaying structured data
15
+ attr_accessor :formatter
16
+
17
+ # The handlers used to display logging info. Defaults to [:console]
18
+ attr_accessor :handlers
19
+
20
+ # The level to log messages at. Default to :info
21
+ attr_accessor :level
22
+
23
+ # The maximum message size to be logged in bytes. Defaults to 8192
24
+ attr_accessor :max_bytes
25
+
26
+ # The maximum depth for formattin array and hash objects. Defaults to 3
27
+ attr_accessor :max_depth
28
+
29
+ # The meta info to display, can be :date, :time, :file, :pid. Defaults to []
30
+ attr_accessor :metadata
31
+
32
+ # The output for the log messages. Default to `stderr`
33
+ attr_accessor :output
34
+
35
+ # Create a configuration instance
36
+ #
37
+ # @api private
38
+ def initialize(**options)
39
+ @max_bytes = options.fetch(:max_bytes) { 2**13 }
40
+ @max_depth = options.fetch(:max_depth) { 3 }
41
+ @level = options.fetch(:level) { :info }
42
+ @metadata = options.fetch(:metadata) { [] }
43
+ @handlers = options.fetch(:handlers) { [:console] }
44
+ @formatter = options.fetch(:formatter) { :text }
45
+ @date_format = options.fetch(:date_format) { "%F" }
46
+ @time_format = options.fetch(:time_format) { "%T.%3N" }
47
+ @output = options.fetch(:output) { $stderr }
48
+ end
49
+
50
+ # Hash representation of this config
51
+ #
52
+ # @return [Hash[Symbol]]
53
+ #
54
+ # @api public
55
+ def to_h
56
+ {
57
+ date_format: date_format,
58
+ formatter: formatter,
59
+ handlers: handlers,
60
+ level: level,
61
+ max_bytes: max_bytes,
62
+ max_depth: max_depth,
63
+ metadata: metadata,
64
+ output: output,
65
+ time_format: time_format
66
+ }
67
+ end
68
+ end # Config
69
+ end # Logger
70
+ 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
33
+ else
34
+ msg
35
+ end
36
+ end
37
+ end
38
+ end # Event
39
+ end # Logger
40
+ end # TTY
@@ -0,0 +1,64 @@
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.reduce({}) do |acc, (k, v)|
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
+ acc
35
+ end
36
+ ::JSON.generate(hash)
37
+ end
38
+
39
+ private
40
+
41
+ def dump_val(val, depth)
42
+ case val
43
+ when Hash then enc_obj(val, depth - 1)
44
+ when Array then enc_arr(val, depth - 1)
45
+ else
46
+ val
47
+ end
48
+ end
49
+
50
+ def enc_obj(obj, depth)
51
+ return ELLIPSIS if depth.zero?
52
+
53
+ obj.reduce({}) { |acc, (k, v)| acc[k] = dump_val(v, depth); acc }
54
+ end
55
+
56
+ def enc_arr(obj, depth)
57
+ return ELLIPSIS if depth.zero?
58
+
59
+ obj.reduce([]) { |acc, v| acc << dump_val(v, depth); acc }
60
+ end
61
+ end # JSON
62
+ end # Formatters
63
+ end # Logger
64
+ end # TTY
@@ -0,0 +1,129 @@
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.reduce([]) do |acc, (k, v)|
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 + (acc[-1].bytesize - ELLIPSIS.bytesize) > max_bytes
41
+ acc.pop
42
+ end
43
+ acc << ELLIPSIS
44
+ break acc
45
+ else
46
+ bytesize += str.bytesize
47
+ acc << str
48
+ end
49
+ acc
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)}" }.join(SPACE) +
90
+ 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, LITERAL_NIL, NUM_REGEX
117
+ ESCAPE_DOUBLE_QUOTE + str.inspect[1..-2] + ESCAPE_DOUBLE_QUOTE
118
+ else
119
+ str
120
+ end
121
+ end
122
+
123
+ def enc_time(time)
124
+ time.strftime("%FT%T%:z")
125
+ end
126
+ end # Text
127
+ end # Formatters
128
+ end # Logger
129
+ end # TTY
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Logger
5
+ module Handlers
6
+ module Base
7
+ # Coerce formatter name into constant
8
+ #
9
+ # @api private
10
+ def coerce_formatter(name)
11
+ case name
12
+ when String, Symbol
13
+ const_name = if Formatters.const_defined?(name.upcase)
14
+ name.upcase
15
+ else
16
+ name.capitalize
17
+ end
18
+ Formatters.const_get(const_name)
19
+ when Class
20
+ name
21
+ else
22
+ raise_formatter_error(name)
23
+ end
24
+ rescue NameError
25
+ raise_formatter_error(name)
26
+ end
27
+
28
+ # Raise error when unknown formatter name
29
+ #
30
+ # @api private
31
+ def raise_formatter_error(name)
32
+ raise Error, "Unrecognized formatter name '#{name.inspect}'"
33
+ end
34
+
35
+ # Metadata for the log event
36
+ #
37
+ # @return [Array[Symbol]]
38
+ #
39
+ # @api private
40
+ def metadata
41
+ if config.metadata.include?(:all)
42
+ [:pid, :date, :time, :file]
43
+ else
44
+ config.metadata
45
+ end
46
+ end
47
+
48
+ # Format path from event metadata
49
+ #
50
+ # @return [String]
51
+ #
52
+ # @api private
53
+ def format_filepath(event)
54
+ "%s:%d:in`%s`" % [event.metadata[:path], event.metadata[:lineno],
55
+ event.metadata[:method]]
56
+ end
57
+ end # Base
58
+ end # Handlers
59
+ end # Logger
60
+ end # TTY
@@ -0,0 +1,155 @@
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
+ }
59
+
60
+ attr_reader :output
61
+
62
+ attr_reader :config
63
+
64
+ attr_reader :level
65
+
66
+ def initialize(output: $stderr, formatter: nil, config: nil, level: nil,
67
+ styles: {})
68
+ @output = Array[output].flatten
69
+ @formatter = coerce_formatter(formatter || config.formatter).new
70
+ @config = config
71
+ @styles = styles
72
+ @level = level || @config.level
73
+ @mutex = Mutex.new
74
+ @pastel = Pastel.new
75
+ end
76
+
77
+ # Handle log event output in format
78
+ #
79
+ # @param [Event] event
80
+ # the current event logged
81
+ #
82
+ # @api public
83
+ def call(event)
84
+ @mutex.lock
85
+
86
+ style = configure_styles(event)
87
+ color = configure_color(style)
88
+
89
+ fmt = []
90
+ metadata.each do |meta|
91
+ case meta
92
+ when :date
93
+ fmt << @pastel.white("[" + event.metadata[:time].
94
+ strftime(config.date_format) + "]")
95
+ when :time
96
+ fmt << @pastel.white("[" + event.metadata[:time].
97
+ strftime(config.time_format) + "]")
98
+ when :file
99
+ fmt << @pastel.white("[#{format_filepath(event)}]")
100
+ when :pid
101
+ fmt << @pastel.white("[%d]" % event.metadata[:pid])
102
+ else
103
+ raise "Unknown metadata `#{meta}`"
104
+ end
105
+ end
106
+ fmt << ARROW unless config.metadata.empty?
107
+ fmt << color.(style[:symbol])
108
+ fmt << color.(style[:label]) + (" " * style[:levelpad])
109
+ fmt << "%-25s" % event.message.join(" ")
110
+ unless event.fields.empty?
111
+ fmt << @formatter.dump(event.fields, max_bytes: config.max_bytes,
112
+ max_depth: config.max_depth).
113
+ gsub(/(\S+)(?=\=)/, color.("\\1")).
114
+ gsub(/\"([^,]+?)\"(?=:)/, "\"" + color.("\\1") + "\"")
115
+ end
116
+ unless event.backtrace.empty?
117
+ fmt << "\n" + format_backtrace(event)
118
+ end
119
+
120
+ output.each { |out| out.puts fmt.join(" ") }
121
+ ensure
122
+ @mutex.unlock
123
+ end
124
+
125
+ private
126
+
127
+ def format_backtrace(event)
128
+ indent = " " * 4
129
+ event.backtrace.map do |bktrace|
130
+ indent + bktrace.to_s
131
+ end.join("\n")
132
+ end
133
+
134
+ # Merge default styles with custom style overrides
135
+ #
136
+ # @return [Hash[String]]
137
+ # the style matching log type
138
+ #
139
+ # @api private
140
+ def configure_styles(event)
141
+ style = STYLES[event.metadata[:name].to_sym].dup
142
+ (@styles[event.metadata[:name].to_sym] || {}).each do |k, v|
143
+ style[k] = v
144
+ end
145
+ style
146
+ end
147
+
148
+ def configure_color(style)
149
+ color = style.fetch(:color) { :cyan }
150
+ @pastel.send(color).detach
151
+ end
152
+ end # Console
153
+ end # Handlers
154
+ end # Logger
155
+ end # TTY
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Logger
5
+ module Handlers
6
+ class Null
7
+ def initialize(*)
8
+ end
9
+
10
+ def call(*)
11
+ # noop
12
+ end
13
+ end # Null
14
+ end # Handlers
15
+ end # Logger
16
+ end # TTY
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module TTY
6
+ class Logger
7
+ module Handlers
8
+ class Stream
9
+ include Base
10
+
11
+ attr_reader :output
12
+
13
+ attr_reader :config
14
+
15
+ attr_reader :level
16
+
17
+ def initialize(output: $stderr, formatter: nil, config: nil, level: nil)
18
+ @output = Array[output].flatten
19
+ @formatter = coerce_formatter(formatter || config.formatter).new
20
+ @config = config
21
+ @level = level || @config.level
22
+ @mutex = Mutex.new
23
+ end
24
+
25
+ # @api public
26
+ def call(event)
27
+ @mutex.lock
28
+
29
+ data = {}
30
+ metadata.each do |meta|
31
+ case meta
32
+ when :date
33
+ data["date"] = event.metadata[:time].strftime(config.date_format)
34
+ when :time
35
+ data["time"] = event.metadata[:time].strftime(config.time_format)
36
+ when :file
37
+ data["path"] = format_filepath(event)
38
+ when :pid
39
+ data["pid"] = event.metadata[:pid]
40
+ else
41
+ raise "Unknown metadata `#{meta}`"
42
+ end
43
+ end
44
+ data["level"] = event.metadata[:level]
45
+ data["message"] = event.message.join(' ')
46
+ unless event.fields.empty?
47
+ data.merge!(event.fields)
48
+ end
49
+ unless event.backtrace.empty?
50
+ data.merge!("backtrace" => event.backtrace.join(","))
51
+ end
52
+
53
+ output.each do |out|
54
+ out.puts @formatter.dump(data, max_bytes: config.max_bytes,
55
+ max_depth: config.max_depth)
56
+ end
57
+ ensure
58
+ @mutex.unlock
59
+ end
60
+ end # Stream
61
+ end # Handlers
62
+ end # Logger
63
+ end # TTY
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Logger
5
+ module Levels
6
+ DEBUG_LEVEL = 0
7
+ INFO_LEVEL = 1
8
+ WARN_LEVEL = 2
9
+ ERROR_LEVEL = 3
10
+ FATAL_LEVEL = 4
11
+
12
+ LEVEL_NAMES = {
13
+ DEBUG_LEVEL => :debug,
14
+ INFO_LEVEL => :info,
15
+ WARN_LEVEL => :warn,
16
+ ERROR_LEVEL => :error,
17
+ FATAL_LEVEL => :fatal
18
+ }
19
+
20
+ def level_names
21
+ [:debug, :info, :warn, :error, :fatal]
22
+ end
23
+
24
+ # @api private
25
+ def level_to_number(level)
26
+ case level.to_s.downcase
27
+ when "debug" then DEBUG_LEVEL
28
+ when "info" then INFO_LEVEL
29
+ when "warn" then WARN_LEVEL
30
+ when "error" then ERROR_LEVEL
31
+ when "fatal" then FATAL_LEVEL
32
+ else
33
+ raise ArgumentError, "Invalid level #{level.inspect}"
34
+ end
35
+ end
36
+
37
+ # @api private
38
+ def number_to_level(level)
39
+ LEVEL_NAMES[level]
40
+ end
41
+
42
+ # @api private
43
+ def compare_levels(left, right)
44
+ left = left.is_a?(Integer) ? left : level_to_number(left)
45
+ right = right.is_a?(Integer) ? right : level_to_number(right)
46
+
47
+ return :eq if left == right
48
+ left < right ? :lt : :gt
49
+ end
50
+ end # Levels
51
+ end # Logger
52
+ end # TTY
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TTY
4
4
  class Logger
5
- VERSION = "0.0.0"
5
+ VERSION = "0.1.0"
6
6
  end
7
7
  end