Class: Musa::Logger::Logger

Inherits:
Logger
  • Object
show all
Defined in:
lib/musa-dsl/logger/logger.rb

Overview

Note:

The logger inherits all standard Ruby Logger methods (debug, info, warn, error, fatal).

Note:

Position values are formatted as floating point for readability, even though the sequencer internally uses Rational numbers.

Custom logger that displays sequencer position with log messages.

This logger extends Ruby's standard Logger class to prepend the current sequencer position to each log message, making it easy to track events in musical time during composition and playback.

Features

  • Automatic sequencer position formatting in log output
  • Configurable position precision (integer and decimal digits)
  • Conditional formatting (position only shown when sequencer is provided)
  • Uses InspectNice refinements for better Rational display
  • Defaults to STDERR output with WARN level

Log Format

The formatted log output follows this pattern:

[position]: [LEVEL] [progname] message

Where:

  • position is the sequencer position (only if sequencer provided)
  • LEVEL is the severity level (omitted for DEBUG)
  • progname is the program/module name (optional)
  • message is the actual log message

Examples:

Basic usage without sequencer

require 'musa-dsl'

logger = Musa::Logger::Logger.new
logger.warn "Something happened"
# Output on STDERR: "[WARN]  Something happened"
#
# Two spaces after the level: the progname's slot is empty, not absent.
# And STDERR, not STDOUT -- which is why capturing this in a test means
# building the logger over an IO of your own.

With sequencer integration

require 'musa-dsl'

sequencer = Musa::Sequencer::Sequencer.new(4, 24)
logger = Musa::Logger::Logger.new(sequencer: sequencer)

sequencer.at 4.5r do
  logger.info "Note played"
end

sequencer.run

# Output on STDERR: "  4.500: [INFO]  Note played"

With custom position format

require 'musa-dsl'

sequencer = Musa::Sequencer::Sequencer.new(4, 24)

# 5 integer digits, 2 decimal places
logger = Musa::Logger::Logger.new(
  sequencer: sequencer,
  position_format: 5.2
)
logger.level = Logger::DEBUG

# At position 123.456:
sequencer.at 123.456r do
  logger.debug "Debugging info"
end

sequencer.run

# Output:  123.46: Debugging info

With program name

require 'musa-dsl'

sequencer = Musa::Sequencer::Sequencer.new(4, 24)
logger = Musa::Logger::Logger.new(sequencer: sequencer)
logger.level = Logger::INFO

sequencer.at 4.5r do
  logger.info('MIDIVoice') { "Playing note 60" }
end

sequencer.run

# Output:  4.500: [INFO] [MIDIVoice] Playing note 60

Real-world scenario with multiple components

require 'musa-dsl'

sequencer = Musa::Sequencer::Sequencer.new(4, 24)
logger = Musa::Logger::Logger.new(sequencer: sequencer)
logger.level = Logger::DEBUG

# Different components log at different times
sequencer.at 1 do
  logger.info('Transport') { "Starting playback" }
end

sequencer.at 1.5r do
  logger.debug('Series') { "Evaluating next value" }
end

sequencer.at 2.25r do
  logger.warn('MIDIVoice') { "Note overflow detected" }
end

sequencer.run

# Output on STDERR:
#    1.000: [INFO] [Transport] Starting playback
#    1.500: [Series] Evaluating next value
#    2.250: [WARN] [MIDIVoice] Note overflow detected

Changing log level dynamically

require 'musa-dsl'

sequencer = Musa::Sequencer::Sequencer.new(4, 24)
logger = Musa::Logger::Logger.new(sequencer: sequencer)
logger.level = Logger::DEBUG  # Show all messages

# Later, reduce verbosity
logger.level = Logger::WARN  # Only warnings and errors

See Also:

Instance Method Summary collapse

Constructor Details

#initialize(io = STDERR, sequencer: nil, position_format: nil) ⇒ Logger

Note:

The logger outputs to STDERR by default with level set to WARN.

Note:

The sequencer's position is read at log time, not at event scheduling time. This means the position reflects when the log message is actually generated.

Creates a new logger with optional sequencer integration.

Examples:

Position format examples

require 'musa-dsl'

sequencer = Musa::Sequencer::Sequencer.new(4, 24)

# Different formats for position display:
# 3.3 => "  4.500" (3 digits, 3 decimals) - default
# 5.2 => "  123.46" (5 digits, 2 decimals)
# 2.0 => "  4" (2 digits, no decimals)
# 4.4 => "   4.5000" (4 digits, 4 decimals)

logger_compact = Musa::Logger::Logger.new(sequencer: sequencer, position_format: 2.0)
logger_precise = Musa::Logger::Logger.new(sequencer: sequencer, position_format: 4.4)

Capturing what it writes

require 'stringio'

io = StringIO.new
sequencer = Musa::Sequencer::BaseSequencer.new(4, 24)
logger = Musa::Logger::Logger.new(io, sequencer: sequencer)
logger.level = ::Logger::INFO

sequencer.at(4) { logger.info "First phrase complete" }
sequencer.run

io.string  # => "  4.000: [INFO]  First phrase complete\n"

Parameters:

  • sequencer (Musa::Sequencer::Sequencer, nil) (defaults to: nil)

    sequencer whose position will be displayed in log messages. When nil, position is not shown.

  • position_format (Numeric, nil) (defaults to: nil)

    format specification for position display. The integer part specifies the number of digits before the decimal point, and the decimal part (×10) specifies digits after the decimal point. Defaults to 3.3 (3 integer digits, 3 decimal places).

  • io (IO, String, nil) (defaults to: STDERR)

    where to write; STDERR when omitted. Anything ::Logger accepts, which includes a StringIO -- the reason this parameter exists is that a logger hardwired to STDERR cannot be observed, so its own format had to be tested against a hand-written copy of it.



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/musa-dsl/logger/logger.rb', line 241

def initialize(io = STDERR, sequencer: nil, position_format: nil)
  super io, level: WARN

  # Store sequencer reference for position queries in formatter
  @sequencer = sequencer

  # Store position format specification
  @position_format = position_format || 3.3

  # Custom formatter that integrates sequencer position with log messages.
  #
  # This proc is called by Ruby's Logger for each log entry. It captures
  # @sequencer and @position_format from the enclosing scope to format
  # messages with musical timing information.
  #
  # The formatter constructs messages in the format:
  #   [position]: [LEVEL] [progname] message
  #
  # Position calculation:
  #
  # - Splits position_format into integer and decimal parts
  # - Example: 3.3 => 3 integer digits + 3 decimal digits
  # - Formats sequencer position with calculated precision
  # - Right-aligns position in the allocated width
  #
  # Severity handling:
  #
  # - DEBUG level: severity not shown in output
  # - Other levels: shown as [WARN], [INFO], [ERROR], [FATAL]
  #
  # Spacing:
  #
  # - Adds separator space only if position, level, or progname are present
  # - Empty messages output just a newline
  #
  # @param severity [String] log level (DEBUG, INFO, WARN, ERROR, FATAL)
  # @param time [Time] timestamp of the log event (not used in current implementation)
  # @param progname [String, nil] program/component name
  # @param msg [String, nil] the actual log message
  # @return [String] formatted log line with newline
  self.formatter = proc do |severity, time, progname, msg|
    # Omit severity label for DEBUG level
    level = "[#{severity}] " unless severity == 'DEBUG'

    if msg
      # Calculate and format sequencer position if available
      position = if @sequencer
                   # Extract integer and decimal digit counts from position_format
                   # e.g., 3.3 => integer_digits=3, decimal_digits=3
                   integer_digits = @position_format.to_i
                   decimal_digits = ((@position_format - integer_digits) * 10).round

                   # Format position: total width includes digits + decimal point + ': '
                   # Right-aligned to keep positions visually aligned in logs
                   "%#{integer_digits + decimal_digits + 1}s: " % ("%.#{decimal_digits}f" % @sequencer.position.to_f)
                 end

      # Wrap progname in brackets if provided
      progname = "[#{progname}]" if progname

      # Construct final message with conditional spacing
      "#{position}#{level}#{progname}#{' ' if position || level || progname}#{msg}\n"
    else
      # Empty message case
      "\n"
    end
  end
end

Instance Method Details

#levelInteger

Override level getter to handle encoding compatibility issues.

Ruby's Logger (>= 1.7.0) has an encoding bug in level_override that causes Encoding::CompatibilityError when mixing UTF-8 strings from Musa with Logger's BINARY (ASCII-8BIT) internal strings.

This override catches the encoding error and returns the level directly from the instance variable, bypassing the buggy level_override method.

Returns:

  • (Integer)

    current log level (DEBUG=0, INFO=1, WARN=2, ERROR=3, FATAL=4)



320
321
322
323
324
325
# File 'lib/musa-dsl/logger/logger.rb', line 320

def level
  super
rescue Encoding::CompatibilityError
  # Bypass level_override and return raw level
  @level
end