tty-logger 0.1.0 → 0.2.0

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