module Tina4::Log
def clear_request_id
def clear_request_id @mutex.synchronize { @request_id = nil } end
def colorize(level, line)
def colorize(level, line) color = case level when :debug then COLORS[:cyan] when :info then COLORS[:green] when :warn then COLORS[:yellow] when :error then COLORS[:red] else COLORS[:reset] end "#{color}#{line}#{COLORS[:reset]}" end
def debug(message, context = {})
def debug(message, context = {}) log(:debug, message, context) end
def error(message, context = {})
def error(message, context = {}) log(:error, message, context) end
def format_line(level, message)
def format_line(level, message) level_str = severity_to_level(level) ts = utc_timestamp rid = request_id rid_str = rid ? " [#{rid}]" : "" ctx = @current_context && !@current_context.empty? ? " #{JSON.generate(@current_context)}" : "" "#{ts} [#{level_str.ljust(7)}]#{rid_str} #{message}#{ctx}" end
def info(message, context = {})
def info(message, context = {}) log(:info, message, context) end
def json_line(level, message)
def json_line(level, message) level_str = severity_to_level(level) entry = { timestamp: utc_timestamp, level: level_str, message: message } rid = request_id entry[:request_id] = rid if rid entry[:context] = @current_context if @current_context && !@current_context.empty? JSON.generate(entry) end
def json_mode?
def json_mode? @json_mode end
def log(level, message, context = {})
def log(level, message, context = {}) setup unless @initialized @current_context = context.is_a?(Hash) ? context : {} formatted = format_line(level, message) # Console output respects TINA4_LOG_LEVEL severity = SEVERITY_MAP[level] || 0 if severity >= @console_level if @json_mode $stdout.puts json_line(level, message) else $stdout.puts colorize(level, formatted) end end # File always gets ALL levels, plain text (no ANSI) write_to_file(strip_ansi(formatted)) @current_context = {} end
def production?
def production? env = ENV["TINA4_ENV"] || ENV["RACK_ENV"] || ENV["RUBY_ENV"] || "development" env.downcase == "production" end
def request_id
def request_id @mutex.synchronize { @request_id } end
def resolve_level
def resolve_level env_level = ENV["TINA4_LOG_LEVEL"] || "[TINA4_LOG_ALL]" LEVELS[env_level] || 0 end
def rotate_if_needed
def rotate_if_needed return unless File.exist?(@log_file) begin return if File.size(@log_file) < @max_size rescue SystemCallError return end # Delete the oldest rotated file if it exists oldest = "#{@log_file}.#{@keep}" File.delete(oldest) if File.exist?(oldest) # Shift existing rotated files: .{n} → .{n+1} (@keep - 1).downto(1) do |n| src = "#{@log_file}.#{n}" dst = "#{@log_file}.#{n + 1}" File.rename(src, dst) if File.exist?(src) end # Rename current log to .1 File.rename(@log_file, "#{@log_file}.1") rescue nil end
def set_request_id(id)
def set_request_id(id) @mutex.synchronize { @request_id = id } end
def setup(root_dir = Dir.pwd)
def setup(root_dir = Dir.pwd) @log_dir = File.join(root_dir, "logs") FileUtils.mkdir_p(@log_dir) @max_size_mb = (ENV["TINA4_LOG_MAX_SIZE"] || "10").to_i @max_size = @max_size_mb * 1024 * 1024 @keep = (ENV["TINA4_LOG_KEEP"] || "5").to_i @json_mode = production? @log_file = File.join(@log_dir, "tina4.log") @console_level = resolve_level @request_id = nil @current_context = {} @mutex = Mutex.new @initialized = true end
def severity_to_level(level)
def severity_to_level(level) case level when :debug then "DEBUG" when :info then "INFO" when :warn then "WARNING" when :error then "ERROR" else level.to_s.upcase end end
def strip_ansi(text)
def strip_ansi(text) text.gsub(ANSI_RE, "") end
def utc_timestamp
def utc_timestamp now = Time.now.utc now.strftime("%Y-%m-%dT%H:%M:%S.") + format("%03d", now.usec / 1000) + "Z" end
def warning(message, context = {})
def warning(message, context = {}) log(:warn, message, context) end
def write_to_file(line)
def write_to_file(line) rotate_if_needed begin File.open(@log_file, "a") { |f| f.puts(line) } rescue IOError, SystemCallError # Don't crash on log write failure end end