tty-logger 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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