# frozen_string_literal: true require "yog/logfmt" # Writes structured log lines. class Yog # Get ready to do some yogging. def initialize(generator: Logfmt, output: $stdout, context: {}) @context = context @generator = generator @output = output freeze end # Log a message. Logs any given block's duration as `elapsed=float` ms. def info(msg, **fields) if @output.nil? return (yield if block_given?) end if msg.is_a?(Hash) msg, fields = nil, fields.merge(msg) end start = Time.now.utc prefix = { now: start } result = nil if msg prefix[:msg] = msg end if block_given? result = yield prefix[:duration] = (Time.now - start) * 1000 end combined = prefix.merge(@context).merge(fields) @output.puts(@generator.generate(combined)) result end # Log an error. def error(ex, **fields) info(error: ex.message, type: ex.class, **fields) end # Update the context. def set(**fields) tap { @context.merge!(fields) } end # Create a new log with more context. def with(**fields) self.class.new \ context: @context.merge(fields), generator: @generator, output: @output end ### Helpers for the default log # The log for Yog(), Yog.set(), and Yog.with(). def self.default logs.first end # A stack of receivers with the default log first. def self.logs Thread.current[:logs] ||= [new] end # Add fields to all Yog() calls in the given block. def self.context(**fields) yield logs.unshift(with(fields)) ensure logs.shift end # Update the context of the default log. def self.set(**fields) default.set(fields) end # Create a new log by extending the context of the default log. def self.with(**fields) default.with(fields) end # Write a line to the default log. def self.info(msg, **fields, &block) default.info(msg, fields, &block) end # Log an error to the default log. def self.error(ex, **fields) default.error(ex, fields) end end # Write a line to the default log. def Yog(msg, **fields, &block) case msg when Exception Yog.error(msg, fields) else Yog.info(msg, fields, &block) end end