1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
|
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2019-2025, by Samuel Williams.
# Copyright, 2019, by Bryan Powell.
# Copyright, 2020, by Michael Adams.
# Copyright, 2021, by Robert Schulze.
module Console
UNKNOWN = :unknown
# A log filter which can be used to filter log messages based on severity, subject, and other criteria.
class Filter
if Object.const_defined?(:Ractor) and RUBY_VERSION >= "3.4"
# Define a method which can be shared between ractors.
def self.define_immutable_method(name, &block)
block = Ractor.make_shareable(block)
self.define_method(name, &block)
end
else
# Define a method.
def self.define_immutable_method(name, &block)
define_method(name, &block)
end
end
# Create a new log filter with specific log levels.
#
# ```ruby
# class MyLogger < Console::Filter[debug: 0, okay: 1, bad: 2, terrible: 3]
# ```
#
# @parameter levels [Hash(Symbol, Integer)] A hash of log levels.
def self.[] **levels
klass = Class.new(self)
minimum_level, maximum_level = levels.values.minmax
klass.instance_exec do
const_set(:LEVELS, levels.freeze)
const_set(:MINIMUM_LEVEL, minimum_level)
const_set(:MAXIMUM_LEVEL, maximum_level)
# The default log level for instances of this filter class.
# Set to MINIMUM_LEVEL to allow all messages by default.
const_set(:DEFAULT_LEVEL, minimum_level)
levels.each do |name, level|
const_set(name.to_s.upcase, level)
define_immutable_method(name) do |subject = nil, *arguments, **options, &block|
if self.enabled?(subject, level)
@output.call(subject, *arguments, severity: name, **@options, **options, &block)
end
return nil
end
define_immutable_method("#{name}!") do
@level = level
end
define_immutable_method("#{name}?") do
@level <= level
end
end
end
return klass
end
# Create a new log filter.
#
# @parameter output [Console::Output] The output destination.
# @parameter verbose [Boolean] Enable verbose output.
# @parameter level [Integer] The log level.
# @parameter options [Hash] Additional options.
def initialize(output, verbose: true, level: nil, **options)
@output = output
@verbose = verbose
# Set the log level using the behaviour implemented in `level=`:
if level
self.level = level
else
@level = self.class::DEFAULT_LEVEL
end
@subjects = {}
@options = options
end
# Create a new log filter with the given options, from an existing log filter.
#
# @parameter level [Integer] The log level.
# @parameter verbose [Boolean] Enable verbose output.
# @parameter options [Hash] Additional options.
# @returns [Console::Filter] The new log filter.
def with(level: @level, verbose: @verbose, **options)
dup.tap do |logger|
logger.level = level
logger.verbose! if verbose
logger.options = @options.merge(options)
end
end
# @attribute [Console::Output] The output destination.
attr_accessor :output
# @attribute [Boolean] Whether to enable verbose output.
attr :verbose
# @attribute [Integer] The current log level.
attr :level
# @attribute [Hash(Module, Integer)] The log levels for specific subject (classes).
attr :subjects
# @attribute [Hash] Additional options.
attr_accessor :options
# Set the log level.
#
# @parameter level [Integer | Symbol] The log level.
def level= level
if level.is_a? Symbol
@level = self.class::LEVELS[level]
else
@level = level
end
end
# Set verbose output (enable by default with no arguments).
#
# @parameter value [Boolean] Enable or disable verbose output.
def verbose!(value = true)
@verbose = value
@output.verbose!(value)
end
# Disable all logging.
def off!
@level = self.class::MAXIMUM_LEVEL + 1
end
# Enable all logging.
def all!
@level = self.class::MINIMUM_LEVEL - 1
end
# Filter log messages based on the subject and log level.
#
# You must provide the subject's class, not an instance of the class.
#
# @parameter subject [Module] The subject to filter.
# @parameter level [Integer] The log level.
def filter(subject, level)
unless subject.is_a?(Module)
raise ArgumentError, "Expected a class, got #{subject.inspect}"
end
@subjects[subject] = level
end
# Whether logging is enabled for the given subject and log level.
#
# You can enable and disable logging for classes. This function checks if logging for a given subject is enabled.
#
# @parameter subject [Module | Object] The subject to check.
# @parameter level [Integer] The log level.
# @returns [Boolean] Whether logging is enabled.
def enabled?(subject, level = self.class::MINIMUM_LEVEL)
subject = subject.class unless subject.is_a?(Module)
if specific_level = @subjects[subject]
return level >= specific_level
end
if level >= @level
return true
end
end
# Enable specific log level for the given class.
#
# @parameter name [Module] The class to enable.
def enable(subject, level = self.class::MINIMUM_LEVEL)
# Set the filter level of logging for a given subject which passes all log messages:
filter(subject, level)
end
# Disable logging for the given class.
#
# @parameter name [Module] The class to disable.
def disable(subject)
# Set the filter level of the logging for a given subject which filters all log messages:
filter(subject, self.class::MAXIMUM_LEVEL + 1)
end
# Clear any specific filters for the given class.
#
# @parameter subject [Module] The class to disable.
def clear(subject)
unless subject.is_a?(Module)
raise ArgumentError, "Expected a class, got #{subject.inspect}"
end
@subjects.delete(subject)
end
# Log a message with the given severity.
#
# If the severity is not defined in this filter's LEVELS (e.g., when chaining
# filters with different severity levels), the message is passed through to the
# output without filtering. This allows custom filters to be composed together.
#
# @parameter subject [Object] The subject of the log message.
# @parameter arguments [Array] The arguments to log.
# @parameter options [Hash] Additional options to pass to the output.
# @parameter block [Proc] A block passed to the output.
# @returns [Nil] Always returns nil.
def call(subject, *arguments, **options, &block)
severity = options[:severity] || UNKNOWN
level = self.class::LEVELS[severity]
# If the severity is unknown (level is nil), pass through to output without filtering:
if level.nil? || self.enabled?(subject, level)
@output.call(subject, *arguments, **options, &block)
end
return nil
end
end
end
|