# Copyright © 2025-26 l5yth & contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # frozen_string_literal: true require "logger" require "time" module PotatoMesh # Logging utilities shared across the web application. module Logging LOGGER_NAME = "potato-mesh" # :nodoc: module_function # Build a logger configured with the potato-mesh formatter. # # @param io [#write] destination for log output. # @return [Logger] configured logger instance. def build_logger(io = $stdout) logger = Logger.new(io) logger.progname = LOGGER_NAME logger.formatter = method(:formatter) logger end # Format log entries with a consistent structure understood by the UI. # # @param severity [String] Ruby logger severity constant (e.g., "DEBUG"). # @param time [Time] timestamp when the log entry was created. # @param progname [String, nil] optional application name emitting the log. # @param message [String] body of the log message. # @return [String] formatted log entry. def formatter(severity, time, progname, message) timestamp = time.utc.iso8601(3) body = message.is_a?(String) ? message : message.inspect "[#{timestamp}] [#{progname || LOGGER_NAME}] [#{severity.downcase}] #{body}\n" end # Emit a structured log entry to the provided logger instance. # # @param logger [Logger, nil] logger to emit against. # @param severity [Symbol] target severity (e.g., :debug, :info). # @param message [String] primary message text. # @param context [String, nil] logical component generating the entry. # @param metadata [Hash] supplemental structured data for the log. # @return [void] def log(logger, severity, message, context: nil, **metadata) return unless logger parts = [] parts << "context=#{context}" if context metadata.each do |key, value| parts << format_metadata_pair(key, value) end parts << message logger.public_send(severity, parts.join(" ")) end # Retrieve the canonical logger for the web application. # # @param target [Object, nil] object with optional +settings.logger+ accessor. # @return [Logger, nil] logger instance when available. def logger_for(target = nil) if target.respond_to?(:settings) && target.settings.respond_to?(:logger) return target.settings.logger end if defined?(PotatoMesh::Application) && PotatoMesh::Application.respond_to?(:settings) && PotatoMesh::Application.settings.respond_to?(:logger) return PotatoMesh::Application.settings.logger end nil end # Format metadata key/value pairs for structured logging output. # # @param key [Symbol, String] # @param value [Object] # @return [String] def format_metadata_pair(key, value) "#{key}=#{value.inspect}" end private_class_method :format_metadata_pair end end