tty-logger 0.1.0 → 0.2.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,9 @@
1
+ require_relative "../lib/tty/logger"
2
+
3
+ logger = TTY::Logger.new(fields: {app: "parent", env: "prod"})
4
+ child_logger = logger.copy(app: "child") do |config|
5
+ config.filters = ["logging"]
6
+ end
7
+
8
+ logger.info("Parent logging")
9
+ child_logger.warn("Child logging")
@@ -1,15 +1,15 @@
1
1
  require_relative "../lib/tty/logger"
2
2
 
3
3
  TTY::Logger.configure do |config|
4
- config.max_bytes = 2**5
5
- config.metadata = [:all]
6
- config.handlers = [[:console, formatter: :text]]
4
+ # config.max_bytes = 2**5
5
+ # config.metadata = [:all]
6
+ # config.handlers = [[:console, formatter: :text]]
7
7
  config.level = :debug
8
8
  end
9
9
 
10
10
  logger = TTY::Logger.new(fields: {app: "myapp", env: "prod"})
11
11
 
12
- logger.with(path: "/var/www/example.com").info("Deploying", "code")
12
+ logger.copy(path: "/var/www/example.com").info("Deploying", "code")
13
13
 
14
14
  puts "Levels:"
15
15
 
@@ -0,0 +1,27 @@
1
+ require_relative "../lib/tty/logger"
2
+
3
+ logger = TTY::Logger.new do |config|
4
+ config.types = {thanks: {level: :info}, done: {level: :info}}
5
+ config.handlers = [
6
+ [:console, {
7
+ styles: {
8
+ thanks: {
9
+ symbol: "❤️ ",
10
+ label: "thanks",
11
+ color: :magenta,
12
+ levelpad: 0
13
+ },
14
+ done: {
15
+ symbol: "!!",
16
+ label: "done",
17
+ color: :green,
18
+ levelpad: 2
19
+ }
20
+ }
21
+ }]
22
+ ]
23
+ end
24
+
25
+ logger.info("This is info!")
26
+ logger.thanks("Great work!")
27
+ logger.done("Work done!")
@@ -0,0 +1,9 @@
1
+ require_relative "../lib/tty/logger"
2
+
3
+ logger = TTY::Logger.new do |config|
4
+ config.level = :debug
5
+ end
6
+
7
+ def success
8
+ logger.log(:debug, "Deploying...")
9
+ end
@@ -1,7 +1,7 @@
1
1
  require_relative "../lib/tty/logger"
2
2
 
3
3
  TTY::Logger.configure do |config|
4
- config.max_bytes = 2**5
4
+ config.max_bytes = 2**7
5
5
  config.metadata = [:all]
6
6
  config.handlers = [[:stream, formatter: :text]]
7
7
  config.level = :debug
@@ -9,7 +9,7 @@ end
9
9
 
10
10
  logger = TTY::Logger.new(fields: {app: "myapp", env: "prod"})
11
11
 
12
- logger.with(path: "/var/www/example.com").info("Deploying", "code")
12
+ logger.copy(path: "/var/www/example.com").info("Deploying", "code")
13
13
 
14
14
  puts "Levels:"
15
15
 
@@ -17,6 +17,45 @@ module TTY
17
17
  # Error raised by this logger
18
18
  class Error < StandardError; end
19
19
 
20
+ FILTERED = "[FILTERED]"
21
+
22
+ LOG_TYPES = {
23
+ debug: { level: :debug },
24
+ info: { level: :info },
25
+ warn: { level: :warn },
26
+ error: { level: :error },
27
+ fatal: { level: :fatal },
28
+ success: { level: :info },
29
+ wait: { level: :info }
30
+ }.freeze
31
+
32
+ # Macro to dynamically define log types
33
+ #
34
+ # @api private
35
+ def self.define_level(name, log_level = nil)
36
+ const_level = (LOG_TYPES[name.to_sym] || log_level)[:level]
37
+
38
+ loc = caller_locations(0,1)[0]
39
+ if loc
40
+ file, line = loc.path, loc.lineno + 7
41
+ else
42
+ file, line = __FILE__, __LINE__ + 3
43
+ end
44
+ class_eval(<<-EOL, file, line)
45
+ def #{name}(*msg, &block)
46
+ log(:#{const_level}, *msg, &block)
47
+ end
48
+ EOL
49
+ end
50
+
51
+ define_level :debug
52
+ define_level :info
53
+ define_level :warn
54
+ define_level :error
55
+ define_level :fatal
56
+ define_level :success
57
+ define_level :wait
58
+
20
59
  # Logger configuration instance
21
60
  #
22
61
  # @api public
@@ -36,6 +75,12 @@ module TTY
36
75
  # @example
37
76
  # logger = TTY::Logger.new(output: $stdout)
38
77
  #
78
+ # @param [IO] output
79
+ # the output object, can be stream
80
+ #
81
+ # @param [Hash] fields
82
+ # the data fields for each log message
83
+ #
39
84
  # @api public
40
85
  def initialize(output: nil, fields: {})
41
86
  @fields = fields
@@ -51,11 +96,28 @@ module TTY
51
96
  @output = output || @config.output
52
97
  @ready_handlers = []
53
98
 
99
+ @config.types.each do |name, log_level|
100
+ add_type(name, log_level)
101
+ end
102
+
54
103
  @handlers.each do |handler|
55
104
  add_handler(handler)
56
105
  end
57
106
  end
58
107
 
108
+ # Add new log type
109
+ #
110
+ # @example
111
+ # add_type(:thanks, {level: :info})
112
+ #
113
+ # @api private
114
+ def add_type(name, log_level)
115
+ if respond_to?(name)
116
+ raise Error, "Already defined log type #{name.inspect}"
117
+ end
118
+ self.class.define_level(name, log_level)
119
+ end
120
+
59
121
  # Add handler for logging messages
60
122
  #
61
123
  # @example
@@ -112,18 +174,24 @@ module TTY
112
174
  raise Error, "Handler needs to be a class name or a symbol name"
113
175
  end
114
176
 
115
- # Add structured data
177
+ # Copy this logger
116
178
  #
117
179
  # @example
118
180
  # logger = TTY::Logger.new
119
- # logger.with(app: "myenv", env: "prod").debug("Deplying")
181
+ # child_logger = logger.copy(app: "myenv", env: "prod")
182
+ # child_logger.info("Deploying")
120
183
  #
121
184
  # @return [TTY::Logger]
122
185
  # a new copy of this logger
123
186
  #
124
187
  # @api public
125
- def with(new_fields)
126
- self.class.new(fields: @fields.merge(new_fields), output: @output)
188
+ def copy(new_fields)
189
+ new_config = @config.to_proc.call(Config.new)
190
+ if block_given?
191
+ yield(new_config)
192
+ end
193
+ self.class.new(fields: @fields.merge(new_fields),
194
+ output: @output, &new_config)
127
195
  end
128
196
 
129
197
  # Check current level against another
@@ -137,22 +205,35 @@ module TTY
137
205
 
138
206
  # Log a message given the severtiy level
139
207
  #
208
+ # @example
209
+ # logger.log(:info, "Deployed successfully")
210
+ #
211
+ # @example
212
+ # logger.log(:info) { "Deployed successfully" }
213
+ #
140
214
  # @api public
141
215
  def log(current_level, *msg, **scoped_fields)
216
+ fields_copy = scoped_fields.dup
142
217
  if msg.empty? && block_given?
143
- msg = [yield]
218
+ msg = []
219
+ Array[yield].flatten(1).each do |el|
220
+ el.is_a?(::Hash) ? fields_copy.merge!(el) : msg << el
221
+ end
144
222
  end
145
- loc = caller_locations(2,1)[0]
223
+ top_caller = caller_locations(1,1)[0]
224
+ loc = caller_locations(2,1)[0] || top_caller
225
+ label = top_caller.label
146
226
  metadata = {
147
227
  level: current_level,
148
228
  time: Time.now,
149
229
  pid: Process.pid,
150
- name: caller_locations(1,1)[0].label,
230
+ name: /<top\s+\(required\)>|<main>/ =~ label ? current_level : label,
151
231
  path: loc.path,
152
232
  lineno: loc.lineno,
153
233
  method: loc.base_label
154
234
  }
155
- event = Event.new(msg, @fields.merge(scoped_fields), metadata)
235
+ event = Event.new(filter(*msg), @fields.merge(fields_copy), metadata)
236
+
156
237
  @ready_handlers.each do |handler|
157
238
  level = handler.respond_to?(:level) ? handler.level : @config.level
158
239
  handler.(event) if log?(level, current_level)
@@ -160,57 +241,42 @@ module TTY
160
241
  self
161
242
  end
162
243
 
163
- # Log a message at :debug level
244
+ # Change current log level for the duration of the block
164
245
  #
165
- # @api public
166
- def debug(*msg, &block)
167
- log(:debug, *msg, &block)
168
- end
169
-
170
- # Log a message at :info level
246
+ # @example
247
+ # logger.log_at :debug do
248
+ # logger.debug("logged")
249
+ # end
171
250
  #
172
- # @examples
173
- # logger.info "Successfully deployed"
174
- # logger.info { "Dynamically generated info" }
251
+ # @param [String] tmp_level
252
+ # the temporary log level
175
253
  #
176
254
  # @api public
177
- def info(*msg, &block)
178
- log(:info, *msg, &block)
179
- end
180
-
181
- # Log a message at :warn level
182
- #
183
- # @api public
184
- def warn(*msg, &block)
185
- log(:warn, *msg, &block)
186
- end
187
-
188
- # Log a message at :error level
189
- #
190
- # @api public
191
- def error(*msg, &block)
192
- log(:error, *msg, &block)
255
+ def log_at(tmp_level, &block)
256
+ @ready_handlers.each do |handler|
257
+ handler.log_at(tmp_level, &block)
258
+ end
193
259
  end
194
260
 
195
- # Log a message at :fatal level
261
+ # Filter message parts for any sensitive information and
262
+ # replace with placeholder.
196
263
  #
197
- # @api public
198
- def fatal(*msg, &block)
199
- log(:fatal, *msg, &block)
200
- end
201
-
202
- # Log a message with a success label
264
+ # @param [Array[String]] messages
265
+ # the messages to filter
203
266
  #
204
- # @api public
205
- def success(*msg, &block)
206
- log(:info, *msg, &block)
207
- end
208
-
209
- # Log a message with a wait label
267
+ # @return [Array[String]]
268
+ # the filtered message
210
269
  #
211
- # @api public
212
- def wait(*msg, &block)
213
- log(:info, *msg, &block)
270
+ # @api private
271
+ def filter(*messages)
272
+ messages.reduce([]) do |acc, msg|
273
+ acc << msg.dup.tap do |msg_copy|
274
+ @config.filters.each do |text, placeholder|
275
+ msg_copy.gsub!(text, placeholder || FILTERED)
276
+ end
277
+ end
278
+ acc
279
+ end
214
280
  end
215
281
  end # Logger
216
282
  end # TTY
@@ -11,6 +11,9 @@ module TTY
11
11
  # The format used for time display
12
12
  attr_accessor :time_format
13
13
 
14
+ # The storage of placholders to filter sensitive data out from the logs. Defaults to {}
15
+ attr_accessor :filters
16
+
14
17
  # The format used for displaying structured data
15
18
  attr_accessor :formatter
16
19
 
@@ -29,9 +32,12 @@ module TTY
29
32
  # The meta info to display, can be :date, :time, :file, :pid. Defaults to []
30
33
  attr_accessor :metadata
31
34
 
32
- # The output for the log messages. Default to `stderr`
35
+ # The output for the log messages. Defaults to `stderr`
33
36
  attr_accessor :output
34
37
 
38
+ # The new custom log types. Defaults to `{}`
39
+ attr_accessor :types
40
+
35
41
  # Create a configuration instance
36
42
  #
37
43
  # @api private
@@ -40,11 +46,33 @@ module TTY
40
46
  @max_depth = options.fetch(:max_depth) { 3 }
41
47
  @level = options.fetch(:level) { :info }
42
48
  @metadata = options.fetch(:metadata) { [] }
49
+ @filters = options.fetch(:filters) { {} }
43
50
  @handlers = options.fetch(:handlers) { [:console] }
44
51
  @formatter = options.fetch(:formatter) { :text }
45
52
  @date_format = options.fetch(:date_format) { "%F" }
46
53
  @time_format = options.fetch(:time_format) { "%T.%3N" }
47
54
  @output = options.fetch(:output) { $stderr }
55
+ @types = options.fetch(:types) { {} }
56
+ end
57
+
58
+ # Clone settings
59
+ #
60
+ # @api public
61
+ def to_proc
62
+ -> (config) {
63
+ config.date_format = @date_format.dup
64
+ config.filters = @filters.dup
65
+ config.formatter = @formatter
66
+ config.handlers = @handlers.dup
67
+ config.level = @level
68
+ config.max_bytes = @max_bytes
69
+ config.max_depth = @max_depth
70
+ config.metadata = @metadata.dup
71
+ config.output = @output.dup
72
+ config.time_format = @time_format.dup
73
+ config.types = @types.dup
74
+ config
75
+ }
48
76
  end
49
77
 
50
78
  # Hash representation of this config
@@ -55,6 +83,7 @@ module TTY
55
83
  def to_h
56
84
  {
57
85
  date_format: date_format,
86
+ filters: filters,
58
87
  formatter: formatter,
59
88
  handlers: handlers,
60
89
  level: level,
@@ -62,7 +91,8 @@ module TTY
62
91
  max_depth: max_depth,
63
92
  metadata: metadata,
64
93
  output: output,
65
- time_format: time_format
94
+ time_format: time_format,
95
+ types: types
66
96
  }
67
97
  end
68
98
  end # Config
@@ -4,6 +4,19 @@ module TTY
4
4
  class Logger
5
5
  module Handlers
6
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
+
7
20
  # Coerce formatter name into constant
8
21
  #
9
22
  # @api private
@@ -138,7 +138,7 @@ module TTY
138
138
  #
139
139
  # @api private
140
140
  def configure_styles(event)
141
- style = STYLES[event.metadata[:name].to_sym].dup
141
+ style = STYLES.fetch(event.metadata[:name].to_sym, {}).dup
142
142
  (@styles[event.metadata[:name].to_sym] || {}).each do |k, v|
143
143
  style[k] = v
144
144
  end
@@ -10,35 +10,50 @@ module TTY
10
10
  FATAL_LEVEL = 4
11
11
 
12
12
  LEVEL_NAMES = {
13
- DEBUG_LEVEL => :debug,
14
- INFO_LEVEL => :info,
15
- WARN_LEVEL => :warn,
16
- ERROR_LEVEL => :error,
17
- FATAL_LEVEL => :fatal
13
+ debug: DEBUG_LEVEL,
14
+ info: INFO_LEVEL,
15
+ warn: WARN_LEVEL,
16
+ error: ERROR_LEVEL,
17
+ fatal: FATAL_LEVEL
18
18
  }
19
19
 
20
+ # All the default level names
21
+ #
22
+ # @return [Array[Symbol]]
23
+ #
24
+ # @api private
20
25
  def level_names
21
- [:debug, :info, :warn, :error, :fatal]
26
+ LEVEL_NAMES.keys
22
27
  end
23
28
 
29
+ # Convert level name to level number
30
+ #
31
+ # @param [Symbol] level
32
+ #
33
+ # @return [Integer]
34
+ #
24
35
  # @api private
25
36
  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
37
+ LEVEL_NAMES[level.to_s.downcase.to_sym] ||
38
+ raise(ArgumentError, "Invalid level #{level.inspect}")
35
39
  end
36
40
 
41
+ # Convert level number to level name
42
+ #
43
+ # @param [Integer] number
44
+ #
45
+ # @return [Symbol]
46
+ #
37
47
  # @api private
38
- def number_to_level(level)
39
- LEVEL_NAMES[level]
48
+ def number_to_level(number)
49
+ LEVEL_NAMES.key(number)
40
50
  end
41
51
 
52
+ # Compares two levels by name or number
53
+ #
54
+ # @return [Symbol]
55
+ # either :lt, :gt or :eq
56
+ #
42
57
  # @api private
43
58
  def compare_levels(left, right)
44
59
  left = left.is_a?(Integer) ? left : level_to_number(left)