console 1.29.2 → 1.36.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/context/command-line.md +47 -0
  4. data/context/configuration.md +23 -0
  5. data/context/events.md +71 -0
  6. data/context/getting-started.md +170 -0
  7. data/context/index.yaml +28 -0
  8. data/context/integration.md +37 -0
  9. data/lib/console/capture.rb +29 -3
  10. data/lib/console/clock.rb +9 -3
  11. data/lib/console/compatible/logger.rb +34 -3
  12. data/lib/console/config.rb +96 -0
  13. data/lib/console/event/failure.rb +33 -4
  14. data/lib/console/event/generic.rb +22 -1
  15. data/lib/console/event/spawn.rb +39 -7
  16. data/lib/console/event.rb +7 -1
  17. data/lib/console/filter.rb +83 -7
  18. data/lib/console/format/safe.rb +205 -58
  19. data/lib/console/format.rb +5 -1
  20. data/lib/console/interface.rb +18 -10
  21. data/lib/console/logger.rb +25 -38
  22. data/lib/console/output/default.rb +31 -6
  23. data/lib/console/output/failure.rb +9 -2
  24. data/lib/console/output/null.rb +5 -2
  25. data/lib/console/output/sensitive.rb +42 -1
  26. data/lib/console/output/serialized.rb +32 -7
  27. data/lib/console/output/split.rb +12 -1
  28. data/lib/console/output/terminal.rb +68 -15
  29. data/lib/console/output/wrapper.rb +12 -1
  30. data/lib/console/output.rb +14 -3
  31. data/lib/console/progress.rb +56 -9
  32. data/lib/console/resolver.rb +19 -2
  33. data/lib/console/terminal/formatter/failure.rb +18 -6
  34. data/lib/console/terminal/formatter/progress.rb +16 -3
  35. data/lib/console/terminal/formatter/spawn.rb +16 -5
  36. data/lib/console/terminal/formatter.rb +16 -0
  37. data/lib/console/terminal/text.rb +66 -20
  38. data/lib/console/terminal/xterm.rb +18 -3
  39. data/lib/console/terminal.rb +7 -9
  40. data/lib/console/version.rb +2 -2
  41. data/lib/console/warn.rb +2 -2
  42. data/lib/console.rb +2 -1
  43. data/license.md +6 -3
  44. data/readme.md +60 -6
  45. data/releases.md +115 -1
  46. data.tar.gz.sig +0 -0
  47. metadata +20 -12
  48. metadata.gz.sig +0 -0
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "filter"
7
+ require_relative "event"
8
+ require_relative "resolver"
9
+ require_relative "output"
10
+ require_relative "logger"
11
+
12
+ module Console
13
+ # Represents a configuration for the traces library.
14
+ class Config
15
+ PATH = ENV.fetch("CONSOLE_CONFIG_PATH", "config/console.rb")
16
+
17
+ # Load the configuration from the given path.
18
+ # @parameter path [String] The path to the configuration file.
19
+ # @returns [Config] The loaded configuration.
20
+ def self.load(path)
21
+ config = self.new
22
+
23
+ if File.exist?(path)
24
+ config.instance_eval(File.read(path), path)
25
+ end
26
+
27
+ return config
28
+ end
29
+
30
+ # Load the default configuration.
31
+ # @returns [Config] The default configuration.
32
+ def self.default
33
+ @default ||= self.load(PATH).freeze
34
+ end
35
+
36
+ # Set the default log level based on `$DEBUG` and `$VERBOSE`.
37
+ # You can also specify CONSOLE_LEVEL=debug or CONSOLE_LEVEL=info in environment.
38
+ # https://mislav.net/2011/06/ruby-verbose-mode/ has more details about how it all fits together.
39
+ #
40
+ # @parameter env [Hash] The environment to read the log level from.
41
+ # @returns [Integer | Symbol] The default log level.
42
+ def log_level(env = ENV)
43
+ Logger.default_log_level(env)
44
+ end
45
+
46
+ # Controls verbose output using `$VERBOSE`.
47
+ def verbose?(env = ENV)
48
+ !$VERBOSE.nil? || env["CONSOLE_VERBOSE"]
49
+ end
50
+
51
+ # Create an output with the given output and options.
52
+ #
53
+ # @parameter output [IO] The output to write log messages to.
54
+ # @parameter env [Hash] The environment to read configuration from.
55
+ # @parameter options [Hash] Additional options to pass to the output.
56
+ # @returns [Output] The created output.
57
+ def make_output(io = nil, env = ENV, **options)
58
+ Output.new(io, env, **options)
59
+ end
60
+
61
+ # Create a resolver with the given logger.
62
+ #
63
+ # @parameter logger [Logger] The logger to set the log levels on.
64
+ # @returns [Resolver | Nil] The created resolver.
65
+ def make_resolver(logger)
66
+ Resolver.default_resolver(logger)
67
+ end
68
+
69
+ # Create a logger with the given output and options.
70
+ #
71
+ # @parameter output [IO] The output to write log messages to.
72
+ # @parameter env [Hash] The environment to read configuration from.
73
+ # @parameter options [Hash] Additional options to pass to the logger.
74
+ # @returns [Logger] The created logger.
75
+ def make_logger(io = $stderr, env = ENV, **options)
76
+ if options[:verbose].nil?
77
+ options[:verbose] = self.verbose?(env)
78
+ end
79
+
80
+ if options[:level].nil?
81
+ options[:level] = self.log_level(env)
82
+ end
83
+
84
+ output = self.make_output(io, env, **options)
85
+
86
+ logger = Logger.new(output, **options)
87
+
88
+ make_resolver(logger)
89
+
90
+ return logger
91
+ end
92
+
93
+ # Load the default configuration.
94
+ DEFAULT = self.default
95
+ end
96
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
  # Copyright, 2021, by Robert Schulze.
6
6
  # Copyright, 2024, by Patrik Wenger.
7
7
 
@@ -9,33 +9,58 @@ require_relative "generic"
9
9
 
10
10
  module Console
11
11
  module Event
12
- # Represents a failure event.
12
+ # Represents a failure of some kind, usually with an attached exception.
13
13
  #
14
14
  # ```ruby
15
- # Console::Event::Failure.for(exception).emit(self)
15
+ # begin
16
+ # raise "Something went wrong!"
17
+ # rescue => exception
18
+ # Console::Event::Failure.log("Something went wrong!", exception)
19
+ # end
16
20
  # ```
21
+ #
22
+ # Generally, you should use the {Console.error} method to log failures, as it will automatically create a failure event for you.
17
23
  class Failure < Generic
24
+ # For the purpose of efficiently formatting backtraces, we need to know the root directory of the project.
25
+ #
26
+ # @returns [String | Nil] The root directory of the project, or nil if it could not be determined.
18
27
  def self.default_root
19
28
  Dir.getwd
20
29
  rescue # e.g. Errno::EMFILE
21
30
  nil
22
31
  end
23
32
 
33
+ # Create a new failure event for the given exception.
34
+ #
35
+ # @parameter exception [Exception] The exception to log.
24
36
  def self.for(exception)
25
37
  self.new(exception, self.default_root)
26
38
  end
27
39
 
40
+ # Log a failure event with the given exception.
41
+ #
42
+ # @parameter subject [String] The subject of the log message.
43
+ # @parameter exception [Exception] The exception to log.
44
+ # @parameter options [Hash] Additional options pass to the logger output.
28
45
  def self.log(subject, exception, **options)
29
46
  Console.error(subject, **self.for(exception).to_hash, **options)
30
47
  end
31
48
 
49
+ # @attribute [Exception] The exception which caused the failure.
32
50
  attr_reader :exception
33
51
 
34
- def initialize(exception, root = Dir.getwd)
52
+ # Create a new failure event for the given exception.
53
+ #
54
+ # @parameter exception [Exception] The exception to log.
55
+ # @parameter root [String] The root directory of the project.
56
+ def initialize(exception, root = self.class.default_root)
35
57
  @exception = exception
36
58
  @root = root
37
59
  end
38
60
 
61
+ # Convert the failure event to a hash.
62
+ #
63
+ # @returns [Hash] The hash representation of the failure event.
39
64
  def to_hash
40
65
  Hash.new.tap do |hash|
41
66
  hash[:type] = :failure
@@ -44,6 +69,10 @@ module Console
44
69
  end
45
70
  end
46
71
 
72
+ # Log the failure event.
73
+ #
74
+ # @parameter arguments [Array] The arguments to log.
75
+ # @parameter options [Hash] Additional options to pass to the logger output.
47
76
  def emit(*arguments, **options)
48
77
  options[:severity] ||= :error
49
78
 
@@ -1,23 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  module Console
7
7
  module Event
8
+ # A generic event which can be used to represent structured data.
8
9
  class Generic
10
+ # Convert the event to a hash suitable for JSON serialization.
11
+ #
12
+ # @returns [Hash] The hash representation of the event.
13
+ def to_hash
14
+ {}
15
+ end
16
+
17
+ # Convert the event to a hash suitable for JSON serialization.
18
+ #
19
+ # @returns [Hash] The hash representation of the event.
9
20
  def as_json(...)
10
21
  to_hash
11
22
  end
12
23
 
24
+ # Serialize the event to JSON.
25
+ #
26
+ # @returns [String] The JSON representation of the event.
13
27
  def to_json(...)
14
28
  JSON.generate(as_json, ...)
15
29
  end
16
30
 
31
+ # Convert the event to a string (JSON).
32
+ #
33
+ # @returns [String] The string representation of the event.
17
34
  def to_s
18
35
  to_json
19
36
  end
20
37
 
38
+ # Log the event using the given output interface.
39
+ #
40
+ # @parameter arguments [Array] The arguments to log.
41
+ # @parameter options [Hash] Additional options to pass to the logger output.
21
42
  def emit(*arguments, **options)
22
43
  Console.call(*arguments, event: self, **options)
23
44
  end
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "generic"
7
7
  require_relative "../clock"
8
8
 
9
9
  module Console
10
10
  module Event
11
- # Represents a spawn event.
11
+ # Represents a child process spawn event.
12
12
  #
13
13
  # ```ruby
14
14
  # Console.info(self, **Console::Event::Spawn.for("ls", "-l"))
@@ -17,6 +17,11 @@ module Console
17
17
  # event.status = Process.wait
18
18
  # ```
19
19
  class Spawn < Generic
20
+ # Create a new spawn event.
21
+ #
22
+ # @parameter arguments [Array] The arguments to the command, similar to how you would pass them to `Kernel.system` or `Process.spawn`.
23
+ # @parameter options [Hash] The options to pass to the command, similar to how you would pass them to `Kernel.system` or `Process.spawn`.
24
+ # @returns [Spawn] The new spawn event representing the command.
20
25
  def self.for(*arguments, **options)
21
26
  # Extract out the command environment:
22
27
  if arguments.first.is_a?(Hash)
@@ -27,6 +32,11 @@ module Console
27
32
  end
28
33
  end
29
34
 
35
+ # Create a new spawn event.
36
+ #
37
+ # @parameter environment [Hash] The environment to use when running the command.
38
+ # @parameter arguments [Array] The arguments used for command execution.
39
+ # @parameter options [Hash] The options to pass to the command, similar to how you would pass them to `Kernel.system` or `Process.spawn`.
30
40
  def initialize(environment, arguments, options)
31
41
  @environment = environment
32
42
  @arguments = arguments
@@ -38,12 +48,35 @@ module Console
38
48
  @status = nil
39
49
  end
40
50
 
51
+ # @attribute [Numeric] The start time of the command.
52
+ attr :start_time
53
+
54
+ # @attribute [Numeric] The end time of the command.
55
+ attr :end_time
56
+
57
+ # @attribute [Process::Status] The status of the command, if it has completed.
58
+ attr :status
59
+
60
+ # Set the status of the command, and record the end time.
61
+ #
62
+ # @parameter status [Process::Status] The status of the command.
63
+ def status=(status)
64
+ @end_time = Time.now
65
+ @status = status
66
+ end
67
+
68
+ # Calculate the duration of the command, if it has completed.
69
+ #
70
+ # @returns [Numeric] The duration of the command.
41
71
  def duration
42
72
  if @end_time
43
73
  @end_time - @start_time
44
74
  end
45
75
  end
46
76
 
77
+ # Convert the spawn event to a hash suitable for JSON serialization.
78
+ #
79
+ # @returns [Hash] The hash representation of the spawn event.
47
80
  def to_hash
48
81
  Hash.new.tap do |hash|
49
82
  hash[:type] = :spawn
@@ -59,15 +92,14 @@ module Console
59
92
  end
60
93
  end
61
94
 
95
+ # Log the spawn event.
96
+ #
97
+ # @parameter arguments [Array] The arguments to log.
98
+ # @parameter options [Hash] Additional options to pass to the logger output.
62
99
  def emit(*arguments, **options)
63
100
  options[:severity] ||= :info
64
101
  super
65
102
  end
66
-
67
- def status=(status)
68
- @end_time = Time.now
69
- @status = status
70
- end
71
103
  end
72
104
  end
73
105
  end
data/lib/console/event.rb CHANGED
@@ -1,7 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "event/spawn"
7
7
  require_relative "event/failure"
8
+
9
+ module Console
10
+ # Structured event logging.
11
+ module Event
12
+ end
13
+ end
@@ -1,26 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
  # Copyright, 2019, by Bryan Powell.
6
6
  # Copyright, 2020, by Michael Adams.
7
7
  # Copyright, 2021, by Robert Schulze.
8
+ # Copyright, 2026, by Copilot.
8
9
 
9
10
  module Console
10
11
  UNKNOWN = :unknown
11
12
 
13
+ # A log filter which can be used to filter log messages based on severity, subject, and other criteria.
12
14
  class Filter
13
- if Object.const_defined?(:Ractor) and RUBY_VERSION >= "3.1"
15
+ if Object.const_defined?(:Ractor) and RUBY_VERSION >= "3.4"
16
+ # Define a method which can be shared between ractors.
14
17
  def self.define_immutable_method(name, &block)
15
18
  block = Ractor.make_shareable(block)
16
19
  self.define_method(name, &block)
17
20
  end
18
21
  else
22
+ # Define a method.
19
23
  def self.define_immutable_method(name, &block)
20
24
  define_method(name, &block)
21
25
  end
22
26
  end
23
27
 
28
+ # Create a new log filter with specific log levels.
29
+ #
30
+ # ```ruby
31
+ # class MyLogger < Console::Filter[debug: 0, okay: 1, bad: 2, terrible: 3]
32
+ # ```
33
+ #
34
+ # @parameter levels [Hash(Symbol, Integer)] A hash of log levels.
24
35
  def self.[] **levels
25
36
  klass = Class.new(self)
26
37
  minimum_level, maximum_level = levels.values.minmax
@@ -30,6 +41,10 @@ module Console
30
41
  const_set(:MINIMUM_LEVEL, minimum_level)
31
42
  const_set(:MAXIMUM_LEVEL, maximum_level)
32
43
 
44
+ # The default log level for instances of this filter class.
45
+ # Set to MINIMUM_LEVEL to allow all messages by default.
46
+ const_set(:DEFAULT_LEVEL, minimum_level)
47
+
33
48
  levels.each do |name, level|
34
49
  const_set(name.to_s.upcase, level)
35
50
 
@@ -54,16 +69,34 @@ module Console
54
69
  return klass
55
70
  end
56
71
 
57
- def initialize(output, verbose: true, level: self.class::DEFAULT_LEVEL, **options)
72
+ # Create a new log filter.
73
+ #
74
+ # @parameter output [Console::Output] The output destination.
75
+ # @parameter verbose [Boolean] Enable verbose output.
76
+ # @parameter level [Integer] The log level.
77
+ # @parameter options [Hash] Additional options.
78
+ def initialize(output, verbose: true, level: nil, **options)
58
79
  @output = output
59
80
  @verbose = verbose
60
- @level = level
81
+
82
+ # Set the log level using the behaviour implemented in `level=`:
83
+ if level
84
+ self.level = level
85
+ else
86
+ @level = self.class::DEFAULT_LEVEL
87
+ end
61
88
 
62
89
  @subjects = {}
63
90
 
64
91
  @options = options
65
92
  end
66
93
 
94
+ # Create a new log filter with the given options, from an existing log filter.
95
+ #
96
+ # @parameter level [Integer] The log level.
97
+ # @parameter verbose [Boolean] Enable verbose output.
98
+ # @parameter options [Hash] Additional options.
99
+ # @returns [Console::Filter] The new log filter.
67
100
  def with(level: @level, verbose: @verbose, **options)
68
101
  dup.tap do |logger|
69
102
  logger.level = level
@@ -72,14 +105,24 @@ module Console
72
105
  end
73
106
  end
74
107
 
108
+ # @attribute [Console::Output] The output destination.
75
109
  attr_accessor :output
110
+
111
+ # @attribute [Boolean] Whether to enable verbose output.
76
112
  attr :verbose
113
+
114
+ # @attribute [Integer] The current log level.
77
115
  attr :level
78
116
 
117
+ # @attribute [Hash(Module, Integer)] The log levels for specific subject (classes).
79
118
  attr :subjects
80
119
 
120
+ # @attribute [Hash] Additional options.
81
121
  attr_accessor :options
82
122
 
123
+ # Set the log level.
124
+ #
125
+ # @parameter level [Integer | Symbol] The log level.
83
126
  def level= level
84
127
  if level.is_a? Symbol
85
128
  @level = self.class::LEVELS[level]
@@ -88,19 +131,30 @@ module Console
88
131
  end
89
132
  end
90
133
 
134
+ # Set verbose output (enable by default with no arguments).
135
+ #
136
+ # @parameter value [Boolean] Enable or disable verbose output.
91
137
  def verbose!(value = true)
92
138
  @verbose = value
93
139
  @output.verbose!(value)
94
140
  end
95
141
 
142
+ # Disable all logging.
96
143
  def off!
97
144
  @level = self.class::MAXIMUM_LEVEL + 1
98
145
  end
99
146
 
147
+ # Enable all logging.
100
148
  def all!
101
149
  @level = self.class::MINIMUM_LEVEL - 1
102
150
  end
103
151
 
152
+ # Filter log messages based on the subject and log level.
153
+ #
154
+ # You must provide the subject's class, not an instance of the class.
155
+ #
156
+ # @parameter subject [Module] The subject to filter.
157
+ # @parameter level [Integer] The log level.
104
158
  def filter(subject, level)
105
159
  unless subject.is_a?(Module)
106
160
  raise ArgumentError, "Expected a class, got #{subject.inspect}"
@@ -109,8 +163,13 @@ module Console
109
163
  @subjects[subject] = level
110
164
  end
111
165
 
166
+ # Whether logging is enabled for the given subject and log level.
167
+ #
112
168
  # You can enable and disable logging for classes. This function checks if logging for a given subject is enabled.
113
- # @param subject [Object] the subject to check.
169
+ #
170
+ # @parameter subject [Module | Object] The subject to check.
171
+ # @parameter level [Integer] The log level.
172
+ # @returns [Boolean] Whether logging is enabled.
114
173
  def enabled?(subject, level = self.class::MINIMUM_LEVEL)
115
174
  subject = subject.class unless subject.is_a?(Module)
116
175
 
@@ -124,19 +183,24 @@ module Console
124
183
  end
125
184
 
126
185
  # Enable specific log level for the given class.
186
+ #
127
187
  # @parameter name [Module] The class to enable.
128
188
  def enable(subject, level = self.class::MINIMUM_LEVEL)
129
189
  # Set the filter level of logging for a given subject which passes all log messages:
130
190
  filter(subject, level)
131
191
  end
132
192
 
193
+ # Disable logging for the given class.
194
+ #
195
+ # @parameter name [Module] The class to disable.
133
196
  def disable(subject)
134
197
  # Set the filter level of the logging for a given subject which filters all log messages:
135
198
  filter(subject, self.class::MAXIMUM_LEVEL + 1)
136
199
  end
137
200
 
138
201
  # Clear any specific filters for the given class.
139
- # @parameter name [Module] The class to disable.
202
+ #
203
+ # @parameter subject [Module] The class to disable.
140
204
  def clear(subject)
141
205
  unless subject.is_a?(Module)
142
206
  raise ArgumentError, "Expected a class, got #{subject.inspect}"
@@ -145,11 +209,23 @@ module Console
145
209
  @subjects.delete(subject)
146
210
  end
147
211
 
212
+ # Log a message with the given severity.
213
+ #
214
+ # If the severity is not defined in this filter's LEVELS (e.g., when chaining
215
+ # filters with different severity levels), the message is passed through to the
216
+ # output without filtering. This allows custom filters to be composed together.
217
+ #
218
+ # @parameter subject [Object] The subject of the log message.
219
+ # @parameter arguments [Array] The arguments to log.
220
+ # @parameter options [Hash] Additional options to pass to the output.
221
+ # @parameter block [Proc] A block passed to the output.
222
+ # @returns [Nil] Always returns nil.
148
223
  def call(subject, *arguments, **options, &block)
149
224
  severity = options[:severity] || UNKNOWN
150
225
  level = self.class::LEVELS[severity]
151
226
 
152
- if self.enabled?(subject, level)
227
+ # If the severity is unknown (level is nil), pass through to output without filtering:
228
+ if level.nil? || self.enabled?(subject, level)
153
229
  @output.call(subject, *arguments, **options, &block)
154
230
  end
155
231