# lib/log.rb # Minimal structured logger for the scraping pipeline. # # Usage: # require_relative "log" # Log.info "geocode", "geocoded DA0306/2025 -> 42 Main St, Hobart" # Log.warn "enrich", "lookup failed for da_foo DA123: connection refused" # Log.debug "migrate", "column address_std already exists — skipped" # Log.error "db", "prepare failed: unknown column 'foo'" # # Output format (no timestamp — Docker/systemd adds one): # INFO [geocode] geocoded DA0306/2025 -> 42 Main St, Hobart # WARN [enrich] lookup failed for da_foo DA123: connection refused # # Verbosity is controlled by the LOG_LEVEL environment variable: # LOG_LEVEL=debug — all messages # LOG_LEVEL=info — info, warn, error (default) # LOG_LEVEL=warn — warn and error only # LOG_LEVEL=error — errors only # # INFO and DEBUG go to $stdout; WARN and ERROR go to $stderr so that # docker compose logs shows them on the correct stream. module Log LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }.freeze # Flush immediately — important in Docker where stdout may be block-buffered. $stdout.sync = true $stderr.sync = true def self.debug(component, msg) = emit(:debug, component, msg) def self.info(component, msg) = emit(:info, component, msg) def self.warn(component, msg) = emit(:warn, component, msg) def self.error(component, msg) = emit(:error, component, msg) # --------------------------------------------------------------------------- def self.min_level key = ENV.fetch("LOG_LEVEL", "info").strip.downcase.to_sym LEVELS.fetch(key, LEVELS[:info]) end private_class_method :min_level def self.emit(level, component, msg) return if LEVELS.fetch(level) < min_level label = level.to_s.upcase.ljust(5) stream = (level == :debug || level == :info) ? $stdout : $stderr stream.puts "#{label} [#{component}] #{msg}" end private_class_method :emit end