console 1.9.0 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79f9ed13eedb54202ce2111a451e6e7d744f97db971a6b619ba3decb5aa6c963
4
- data.tar.gz: 8c77221c0e50be91b7c1857fde7b596ec7e48eebd67a43287bd01aa3f95e70b4
3
+ metadata.gz: 5928a2a5d4c07c44e8ad937edbab2b8fa326ff13553ffb4bf89c0854184f1bd8
4
+ data.tar.gz: e0495d1933a3a0991b23193aae3988fb679389f02514d57c9a15ce7ccbf73497
5
5
  SHA512:
6
- metadata.gz: 021e0bd5c98d7019af39186b6fa06e49aed619cb5228051a85725bd43f3f05d8015b6d9bc84baf3a49f51a9b13ac62ed65cb20051f9561939d0fb72c791b8475
7
- data.tar.gz: '029abdaded5a4d383e7e4b666c99e2b06c0aaabed87c1da9740c6925ca5e9308eb41b52d61835e0b6a3223efff9b25a98aac555fe43c2ca441ef48f5f27e6f3f'
6
+ metadata.gz: e69af353f0397d3620654d9108b2cb7fffe4d601e4a4f9feea7cdf09a7cd40aa3f310faded79abfc8de5735c257b59713bb9d5a91d5320a1dca15d0df4a92804
7
+ data.tar.gz: c46ec0e33e56260345710facc882d89edb4782179ccdb7ec97ed70c3d83087465b4758ceecdaa7f8ac4e2508dc18f46968974bee19a694c62c5314157a8402fe
data/lib/console.rb CHANGED
@@ -20,83 +20,24 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
21
  # THE SOFTWARE.
22
22
 
23
+ require_relative 'console/version'
23
24
  require_relative 'console/logger'
24
- require_relative 'console/resolver'
25
- require_relative 'console/terminal/logger'
26
25
 
27
26
  module Console
28
- class << self
29
- attr_accessor :logger
30
-
31
- # Set the default log level based on `$DEBUG` and `$VERBOSE`.
32
- # You can also specify CONSOLE_LEVEL=debug or CONSOLE_LEVEL=info in environment.
33
- # https://mislav.net/2011/06/ruby-verbose-mode/ has more details about how it all fits together.
34
- def default_log_level(env = ENV)
35
- if level = (env['CONSOLE_LEVEL'] || env['CONSOLE_LOG_LEVEL'])
36
- Logger::LEVELS[level.to_sym] || Logger.warn
37
- elsif $DEBUG
38
- Logger::DEBUG
39
- elsif $VERBOSE.nil?
40
- Logger::WARN
41
- else
42
- Logger::INFO
43
- end
44
- end
45
-
46
- # You can change the log level for different classes using CONSOLE_<LEVEL> env vars.
47
- #
48
- # e.g. `CONSOLE_WARN=Acorn,Banana CONSOLE_DEBUG=Cat` will set the log level for Acorn and Banana to warn and Cat to
49
- # debug. This overrides the default log level.
50
- #
51
- # @param logger [Logger] A logger instance to set the logging levels on.
52
- # @param env [Hash] Environment to read levels from.
53
- #
54
- # @return [nil] if there were no custom logging levels specified in the environment.
55
- # @return [Resolver] if there were custom logging levels, then the created resolver is returned.
56
- def default_resolver(logger, env = ENV)
57
- # find all CONSOLE_<LEVEL> variables from environment
58
- levels = Logger::LEVELS
59
- .map { |label, level| [level, env["CONSOLE_#{label.to_s.upcase}"]&.split(',')] }
60
- .to_h
61
- .compact
62
-
63
- # if we have any levels, then create a class resolver, and each time a class is resolved, set the log level for
64
- # that class to the specified level
65
- if levels.any?
66
- resolver = Resolver.new
67
- levels.each do |level, names|
68
- resolver.bind(names) do |klass|
69
- logger.enable(klass, level)
70
- end
71
- end
72
- return resolver
73
- end
74
- end
75
-
76
- # Controls verbose output using `$VERBOSE`.
77
- def verbose?
78
- !$VERBOSE.nil?
79
- end
80
-
81
- def build(output, verbose: self.verbose?, level: self.default_log_level)
82
- terminal = Terminal::Logger.new(output, verbose: verbose)
83
-
84
- logger = Logger.new(terminal, verbose: verbose, level: level)
85
-
86
- return logger
87
- end
27
+ def self.logger
28
+ Logger.instance
88
29
  end
89
30
 
90
- # Create the logger instance:
91
- @logger = self.build($stderr)
92
- @resolver = self.default_resolver(@logger)
31
+ def self.logger= instance
32
+ Logger.instance= instance
33
+ end
93
34
 
94
35
  def logger= logger
95
36
  @logger = logger
96
37
  end
97
38
 
98
39
  def logger
99
- @logger || Console.logger
40
+ @logger || Logger.instance
100
41
  end
101
42
 
102
43
  def self.extended(klass)
@@ -21,15 +21,20 @@
21
21
  require_relative 'filter'
22
22
 
23
23
  module Console
24
+ # A general sink which captures all events into a buffer.
24
25
  class Capture
25
26
  def initialize
26
- @events = []
27
+ @buffer = []
27
28
  end
28
29
 
29
- attr :events
30
+ attr :buffer
30
31
 
31
32
  def last
32
- @events.last
33
+ @buffer.last
34
+ end
35
+
36
+ def clear
37
+ @buffer.clear
33
38
  end
34
39
 
35
40
  def verbose!(value = true)
@@ -37,7 +42,7 @@ module Console
37
42
 
38
43
  def call(subject = nil, *arguments, severity: UNKNOWN, **options, &block)
39
44
  message = {
40
- time: Time.now.iso8601,
45
+ time: ::Time.now.iso8601,
41
46
  severity: severity,
42
47
  **options,
43
48
  }
@@ -60,7 +65,7 @@ module Console
60
65
  end
61
66
  end
62
67
 
63
- @events << message
68
+ @buffer << message
64
69
  end
65
70
  end
66
71
  end
@@ -0,0 +1,50 @@
1
+ # Copyright, 2021, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Console
22
+ module Clock
23
+ def self.formatted_duration(duration)
24
+ if duration < 60.0
25
+ return "#{duration.round(2)}s"
26
+ end
27
+
28
+ duration /= 60.0
29
+
30
+ if duration < 60.0
31
+ return "#{duration.round}m"
32
+ end
33
+
34
+ duration /= 60.0
35
+
36
+ if duration < 60.0
37
+ return "#{duration.round(1)}h"
38
+ end
39
+
40
+ duration /= 24.0
41
+
42
+ return "#{duration.round(1)}d"
43
+ end
44
+
45
+ # Get the current elapsed monotonic time.
46
+ def self.now
47
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
48
+ end
49
+ end
50
+ end
data/lib/console/event.rb CHANGED
@@ -20,4 +20,5 @@
20
20
 
21
21
  require_relative 'event/spawn'
22
22
  require_relative 'event/failure'
23
+ require_relative 'event/metric'
23
24
  require_relative 'event/progress'
@@ -47,6 +47,12 @@ module Console
47
47
  terminal[:exception_title] ||= terminal.style(:red, nil, :bold)
48
48
  terminal[:exception_detail] ||= terminal.style(:yellow)
49
49
  terminal[:exception_backtrace] ||= terminal.style(:red)
50
+ terminal[:exception_backtrace_other] ||= terminal.style(:red, nil, :faint)
51
+ terminal[:exception_message] ||= terminal.style(:default)
52
+ end
53
+
54
+ def to_h
55
+ {exception: @exception, root: @root}
50
56
  end
51
57
 
52
58
  def format(output, terminal, verbose)
@@ -62,13 +68,18 @@ module Console
62
68
  output.puts " #{terminal[:exception_detail]}#{line}#{terminal.reset}"
63
69
  end
64
70
 
71
+ root_pattern = /^#{@root}\// if @root
72
+
65
73
  exception.backtrace&.each_with_index do |line, index|
66
74
  path, offset, message = line.split(":")
75
+ style = :exception_backtrace
67
76
 
68
77
  # Make the path a bit more readable
69
- path.gsub!(/^#{@root}\//, "./") if @root
78
+ if root_pattern and path.sub!(root_pattern, "").nil?
79
+ style = :exception_backtrace_other
80
+ end
70
81
 
71
- output.puts " #{index == 0 ? "→" : " "} #{terminal[:exception_backtrace]}#{path}:#{offset}#{terminal.reset} #{message}"
82
+ output.puts " #{index == 0 ? "→" : " "} #{terminal[style]}#{path}:#{offset}#{terminal[:exception_message]} #{message}#{terminal.reset}"
72
83
  end
73
84
 
74
85
  if exception.cause
@@ -24,6 +24,13 @@ module Console
24
24
  def self.register(terminal)
25
25
  end
26
26
 
27
+ def to_h
28
+ end
29
+
30
+ def as_json
31
+ to_h
32
+ end
33
+
27
34
  def format(buffer, terminal)
28
35
  end
29
36
  end
@@ -0,0 +1,42 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'metric'
22
+ require_relative '../clock'
23
+
24
+ module Console
25
+ module Event
26
+ class Enter < Generic
27
+ def initialize(name)
28
+ @name = name
29
+ end
30
+
31
+ def format(output, terminal, verbose)
32
+ output.puts "→ #{@name}"
33
+ end
34
+ end
35
+
36
+ class Exit < Metric
37
+ def value_string
38
+ "← #{@name} took #{Clock.formatted_duration(@value)}"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,58 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'generic'
22
+ require_relative '../clock'
23
+
24
+ module Console
25
+ module Event
26
+ class Metric < Generic
27
+ def self.[](**parameters)
28
+ parameters.map(&self.method(:new))
29
+ end
30
+
31
+ def initialize(name, value, **tags)
32
+ @name = name
33
+ @value = value
34
+ @tags = tags
35
+ end
36
+
37
+ attr :name
38
+ attr :value
39
+ attr :tags
40
+
41
+ def to_h
42
+ {name: @name, value: @value, tags: @tags}
43
+ end
44
+
45
+ def value_string
46
+ "#{@name}: #{@value}"
47
+ end
48
+
49
+ def format(output, terminal, verbose)
50
+ if @tags&.any?
51
+ output.puts "#{value_string} #{@tags.inspect}"
52
+ else
53
+ output.puts value_string
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -63,7 +63,7 @@ module Console
63
63
  terminal[:progress_bar] ||= terminal.style(:blue, :white)
64
64
  end
65
65
 
66
- def as_json
66
+ def to_h
67
67
  {current: @current, total: @total}
68
68
  end
69
69
 
@@ -24,6 +24,7 @@ module Console
24
24
  module Event
25
25
  class Spawn < Generic
26
26
  def self.for(*arguments, **options)
27
+ # Extract out the command environment:
27
28
  if arguments.first.is_a?(Hash)
28
29
  self.new(*arguments, **options)
29
30
  else
@@ -51,6 +52,10 @@ module Console
51
52
  terminal[:shell_command] ||= terminal.style(:blue, nil, :bold)
52
53
  end
53
54
 
55
+ def to_h
56
+ {environment: @environment, arguments: @arguments, options: @options}
57
+ end
58
+
54
59
  def format(output, terminal, verbose)
55
60
  arguments = @arguments.flatten.collect(&:to_s)
56
61
 
@@ -46,7 +46,7 @@ module Console
46
46
  end
47
47
 
48
48
  define_method("#{name}?") do
49
- @level >= level
49
+ @level <= level
50
50
  end
51
51
  end
52
52
  end
@@ -20,17 +20,72 @@
20
20
 
21
21
  require_relative 'filter'
22
22
  require_relative 'measure'
23
+ require_relative 'progress'
24
+
25
+ require_relative 'resolver'
26
+ require_relative 'terminal/logger'
27
+
28
+ require 'fiber/local'
23
29
 
24
30
  module Console
25
31
  class Logger < Filter[debug: 0, info: 1, warn: 2, error: 3, fatal: 4]
32
+ extend Fiber::Local
33
+
34
+ # Set the default log level based on `$DEBUG` and `$VERBOSE`.
35
+ # You can also specify CONSOLE_LEVEL=debug or CONSOLE_LEVEL=info in environment.
36
+ # https://mislav.net/2011/06/ruby-verbose-mode/ has more details about how it all fits together.
37
+ def self.default_log_level(env = ENV)
38
+ if level = (env['CONSOLE_LEVEL'] || env['CONSOLE_LOG_LEVEL'])
39
+ LEVELS[level.to_sym] || level.to_i
40
+ elsif $DEBUG
41
+ DEBUG
42
+ elsif $VERBOSE.nil?
43
+ WARN
44
+ else
45
+ INFO
46
+ end
47
+ end
48
+
49
+ # Controls verbose output using `$VERBOSE`.
50
+ def self.verbose?(env = ENV)
51
+ !$VERBOSE.nil? || env['CONSOLE_VERBOSE']
52
+ end
53
+
54
+ def self.default_logger(output, verbose: self.verbose?, level: self.default_log_level)
55
+ terminal = Terminal::Logger.new(output, verbose: verbose)
56
+
57
+ logger = self.new(terminal, verbose: verbose, level: level)
58
+ Resolver.default_resolver(logger)
59
+
60
+ return logger
61
+ end
62
+
63
+ def self.local
64
+ self.default_logger($stderr)
65
+ end
66
+
26
67
  DEFAULT_LEVEL = 1
27
68
 
28
69
  def initialize(output, **options)
29
70
  super(output, **options)
30
71
  end
31
72
 
32
- def measure(subject, total)
33
- Measure.new(self, subject, total)
73
+ def progress(subject, total, **options)
74
+ Progress.new(self, subject, total, **options)
75
+ end
76
+
77
+ def measure(subject, name = "block", **options, &block)
78
+ measure = Measure.new(self, subject)
79
+
80
+ if block_given?
81
+ measure.duration(name, **options, &block)
82
+ end
83
+
84
+ return measure
85
+ end
86
+
87
+ def failure(subject, exception, *arguments, &block)
88
+ fatal(subject, *arguments, Event::Failure.new(exception), &block)
34
89
  end
35
90
  end
36
91
  end
@@ -1,4 +1,4 @@
1
- # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
2
  #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  # of this software and associated documentation files (the "Software"), to deal
@@ -18,98 +18,29 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
+ require_relative 'event/measure'
22
+ require_relative 'clock'
23
+
21
24
  module Console
22
25
  class Measure
23
- def initialize(output, subject, total = 0)
26
+ def initialize(output, subject)
24
27
  @output = output
25
28
  @subject = subject
26
-
27
- @start_time = Time.now
28
-
29
- @current = 0
30
- @total = total
31
- end
32
-
33
- attr :subject
34
-
35
- attr :current
36
-
37
- attr :total
38
-
39
- def duration
40
- Time.now - @start_time
41
- end
42
-
43
- def progress
44
- @current.to_f / @total.to_f
45
- end
46
-
47
- def remaining
48
- @total - @current
49
- end
50
-
51
- def average_duration
52
- if @current > 0
53
- duration / @current
54
- end
55
- end
56
-
57
- def estimated_remaining_time
58
- if average_duration = self.average_duration
59
- average_duration * remaining
60
- end
61
- end
62
-
63
- def increment(amount = 1)
64
- @current += amount
65
-
66
- @output.info(@subject, self) {Event::Progress.new(@current, @total)}
67
-
68
- return self
69
29
  end
70
30
 
71
- def resize(total)
72
- @total = total
73
-
74
- @output.info(@subject, self) {Event::Progress.new(@current, @total)}
75
-
76
- return self
77
- end
78
-
79
- def mark(*arguments)
80
- @output.info(@subject, *arguments)
81
- end
82
-
83
- def to_s
84
- if estimated_remaining_time = self.estimated_remaining_time
85
- "#{@current}/#{@total} completed in #{self.formatted_duration self.duration}, #{self.formatted_duration estimated_remaining_time} remaining."
86
- else
87
- "#{@current}/#{@total} completed, waiting for estimate..."
88
- end
89
- end
90
-
91
- private
92
-
93
- def formatted_duration(duration)
94
- if duration < 60.0
95
- return "#{duration.round(2)}s"
96
- end
97
-
98
- duration /= 60.0
31
+ # Measure the execution of a block of code.
32
+ def duration(name, **tags, &block)
33
+ @output.info(@subject) {Event::Enter.new(name)}
99
34
 
100
- if duration < 60.0
101
- return "#{duration.round}m"
102
- end
35
+ start_time = Clock.now
103
36
 
104
- duration /= 60.0
37
+ yield
105
38
 
106
- if duration < 60.0
107
- return "#{duration.round(1)}h"
108
- end
39
+ duration = Clock.now - start_time
109
40
 
110
- duration /= 24.0
41
+ @output.info(@subject) {Event::Exit.new(name, duration, **tags)}
111
42
 
112
- return "#{duration.round(1)}d"
43
+ return duration
113
44
  end
114
45
  end
115
46
  end
@@ -0,0 +1,142 @@
1
+ # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'event/progress'
22
+
23
+ module Console
24
+ class Progress
25
+ def self.now
26
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
27
+ end
28
+
29
+ def initialize(output, subject, total = 0, minimum_output_duration: 1.0)
30
+ @output = output
31
+ @subject = subject
32
+
33
+ @start_time = Progress.now
34
+
35
+ @last_output_time = nil
36
+ @minimum_output_duration = 0.1
37
+
38
+ @current = 0
39
+ @total = total
40
+ end
41
+
42
+ attr :subject
43
+ attr :current
44
+ attr :total
45
+
46
+ def duration
47
+ Progress.now - @start_time
48
+ end
49
+
50
+ def progress
51
+ @current.to_f / @total.to_f
52
+ end
53
+
54
+ def remaining
55
+ @total - @current
56
+ end
57
+
58
+ def average_duration
59
+ if @current > 0
60
+ duration / @current
61
+ end
62
+ end
63
+
64
+ def estimated_remaining_time
65
+ if average_duration = self.average_duration
66
+ average_duration * remaining
67
+ end
68
+ end
69
+
70
+ def increment(amount = 1)
71
+ @current += amount
72
+
73
+ if output?
74
+ @output.info(@subject, self) {Event::Progress.new(@current, @total)}
75
+ @last_output_time = Progress.now
76
+ end
77
+
78
+ return self
79
+ end
80
+
81
+ def resize(total)
82
+ @total = total
83
+
84
+ @output.info(@subject, self) {Event::Progress.new(@current, @total)}
85
+ @last_output_time = Progress.now
86
+
87
+ return self
88
+ end
89
+
90
+ def mark(*arguments)
91
+ @output.info(@subject, *arguments)
92
+ end
93
+
94
+ def to_s
95
+ if estimated_remaining_time = self.estimated_remaining_time
96
+ "#{@current}/#{@total} completed in #{formatted_duration(self.duration)}, #{formatted_duration(estimated_remaining_time)} remaining."
97
+ else
98
+ "#{@current}/#{@total} completed, waiting for estimate..."
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def duration_since_last_output
105
+ if @last_output_time
106
+ Progress.now - @last_output_time
107
+ end
108
+ end
109
+
110
+ def output?
111
+ if remaining.zero?
112
+ return true
113
+ elsif duration = duration_since_last_output
114
+ return duration > @minimum_output_duration
115
+ else
116
+ return true
117
+ end
118
+ end
119
+
120
+ def formatted_duration(duration)
121
+ if duration < 60.0
122
+ return "#{duration.round(2)}s"
123
+ end
124
+
125
+ duration /= 60.0
126
+
127
+ if duration < 60.0
128
+ return "#{duration.round}m"
129
+ end
130
+
131
+ duration /= 60.0
132
+
133
+ if duration < 60.0
134
+ return "#{duration.round(1)}h"
135
+ end
136
+
137
+ duration /= 24.0
138
+
139
+ return "#{duration.round(1)}d"
140
+ end
141
+ end
142
+ end
@@ -22,6 +22,36 @@ require_relative 'filter'
22
22
 
23
23
  module Console
24
24
  class Resolver
25
+ # You can change the log level for different classes using CONSOLE_<LEVEL> env vars.
26
+ #
27
+ # e.g. `CONSOLE_WARN=Acorn,Banana CONSOLE_DEBUG=Cat` will set the log level for the classes Acorn and Banana to `warn` and Cat to `debug`. This overrides the default log level.
28
+ #
29
+ # @parameter logger [Logger] A logger instance to set the logging levels on.
30
+ # @parameter env [Hash] The environment to read levels from.
31
+ #
32
+ # @returns [Nil] If there were no custom logging levels specified in the environment.
33
+ # @returns [Resolver] If there were custom logging levels, then the created resolver is returned.
34
+ def self.default_resolver(logger, env = ENV)
35
+ # Find all CONSOLE_<LEVEL> variables from environment:
36
+ levels = Logger::LEVELS
37
+ .map{|label, level| [level, env["CONSOLE_#{label.upcase}"]&.split(',')]}
38
+ .to_h
39
+ .compact
40
+
41
+ # If we have any levels, then create a class resolver, and each time a class is resolved, set the log level for that class to the specified level:
42
+ if levels.any?
43
+ resolver = Resolver.new
44
+
45
+ levels.each do |level, names|
46
+ resolver.bind(names) do |klass|
47
+ logger.enable(klass, level)
48
+ end
49
+ end
50
+
51
+ return resolver
52
+ end
53
+ end
54
+
25
55
  def initialize
26
56
  @names = {}
27
57
 
@@ -24,6 +24,9 @@ require_relative '../event'
24
24
  require_relative 'text'
25
25
  require_relative 'xterm'
26
26
 
27
+ require 'json'
28
+ require 'fiber'
29
+
27
30
  module Console
28
31
  module Terminal
29
32
  # This, and all related methods, is considered private.
@@ -64,7 +67,6 @@ module Console
64
67
  @verbose = verbose
65
68
  end
66
69
 
67
- @terminal[:logger_prefix] ||= @terminal.style(nil, nil, nil)
68
70
  @terminal[:logger_suffix] ||= @terminal.style(:white, nil, :faint)
69
71
  @terminal[:subject] ||= @terminal.style(nil, nil, :bold)
70
72
  @terminal[:debug] = @terminal.style(:cyan)
@@ -77,7 +79,9 @@ module Console
77
79
  end
78
80
 
79
81
  attr :io
82
+
80
83
  attr_accessor :verbose
84
+
81
85
  attr :start
82
86
  attr :terminal
83
87
 
@@ -94,7 +98,7 @@ module Console
94
98
 
95
99
  UNKNOWN = 'unknown'
96
100
 
97
- def call(subject = nil, *arguments, name: nil, severity: UNKNOWN, &block)
101
+ def call(subject = nil, *arguments, name: nil, severity: UNKNOWN, **options, &block)
98
102
  prefix = build_prefix(name || severity.to_s)
99
103
  indent = " " * prefix.size
100
104
 
@@ -104,6 +108,10 @@ module Console
104
108
  format_subject(severity, prefix, subject, buffer)
105
109
  end
106
110
 
111
+ if options&.any?
112
+ format_options(options, buffer)
113
+ end
114
+
107
115
  arguments.each do |argument|
108
116
  format_argument(argument, buffer)
109
117
  end
@@ -121,10 +129,14 @@ module Console
121
129
 
122
130
  protected
123
131
 
132
+ def format_options(options, output)
133
+ format_value(options.to_json, output)
134
+ end
135
+
124
136
  def format_argument(argument, output)
125
137
  case argument
126
138
  when Exception
127
- Event::Failure.new(argument).format(output, @terminal, @verbose)
139
+ Event::Failure.for(argument).format(output, @terminal, @verbose)
128
140
  when Event::Generic
129
141
  argument.format(output, @terminal, @verbose)
130
142
  else
@@ -142,11 +154,21 @@ module Console
142
154
  end
143
155
  end
144
156
 
157
+ def default_suffix(object = nil)
158
+ buffer = +" #{@terminal[:logger_suffix]}"
159
+
160
+ if object
161
+ buffer << "[oid=0x#{object.object_id.to_s(16)}] "
162
+ end
163
+
164
+ buffer << "[ec=0x#{Fiber.current.object_id.to_s(16)}] [pid=#{Process.pid}] [#{::Time.now}]#{@terminal.reset}"
165
+ end
166
+
145
167
  def format_object_subject(severity, prefix, subject, output)
146
168
  prefix_style = @terminal[severity]
147
169
 
148
170
  if @verbose
149
- suffix = " #{@terminal[:logger_suffix]}[oid=0x#{subject.object_id.to_s(16)}] [pid=#{Process.pid}] [#{Time.now}]#{@terminal.reset}"
171
+ suffix = default_suffix(subject)
150
172
  end
151
173
 
152
174
  prefix = "#{prefix_style}#{prefix}:#{@terminal.reset} "
@@ -158,7 +180,7 @@ module Console
158
180
  prefix_style = @terminal[severity]
159
181
 
160
182
  if @verbose
161
- suffix = " #{@terminal[:logger_suffix]}[pid=#{Process.pid}] [#{Time.now}]#{@terminal.reset}"
183
+ suffix = default_suffix
162
184
  end
163
185
 
164
186
  prefix = "#{prefix_style}#{prefix}:#{@terminal.reset} "
@@ -175,15 +197,7 @@ module Console
175
197
  end
176
198
 
177
199
  def time_offset_prefix
178
- offset = Time.now - @start_at
179
- minutes = (offset/60).floor
180
- seconds = (offset - (minutes*60))
181
-
182
- if minutes > 0
183
- "#{minutes}m#{seconds.floor}s"
184
- else
185
- "#{seconds.round(2)}s"
186
- end.rjust(6)
200
+ Clock.formatted_duration(Time.now - @start_at).rjust(6)
187
201
  end
188
202
 
189
203
  def build_prefix(name)
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Console
22
- VERSION = "1.9.0"
22
+ VERSION = "1.11.0"
23
23
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: console
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-27 00:00:00.000000000 Z
11
+ date: 2021-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: fiber-local
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bake
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -90,14 +104,18 @@ files:
90
104
  - lib/console.rb
91
105
  - lib/console/buffer.rb
92
106
  - lib/console/capture.rb
107
+ - lib/console/clock.rb
93
108
  - lib/console/event.rb
94
109
  - lib/console/event/failure.rb
95
110
  - lib/console/event/generic.rb
111
+ - lib/console/event/measure.rb
112
+ - lib/console/event/metric.rb
96
113
  - lib/console/event/progress.rb
97
114
  - lib/console/event/spawn.rb
98
115
  - lib/console/filter.rb
99
116
  - lib/console/logger.rb
100
117
  - lib/console/measure.rb
118
+ - lib/console/progress.rb
101
119
  - lib/console/resolver.rb
102
120
  - lib/console/serialized/logger.rb
103
121
  - lib/console/split.rb
@@ -125,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
143
  - !ruby/object:Gem::Version
126
144
  version: '0'
127
145
  requirements: []
128
- rubygems_version: 3.1.2
146
+ rubygems_version: 3.2.3
129
147
  signing_key:
130
148
  specification_version: 4
131
149
  summary: Beautiful logging for Ruby.