# 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 write(msg, **fields) if msg.is_a?(Hash) fields, msg = msg, nil end result = if block_given? start = Time.now yield.tap do fields[:duration] = (Time.now - start) * 1000 end end unless @output.nil? prefix = { now: Time.now.utc, msg: msg } combined = Yog.merge(prefix, fields, @context) @output.puts(@generator.generate(combined)) end result end # Log an error. def error(ex, **fields) write(ex.message, at: "error", type: ex.class, **fields) end # Update the context. def push(**fields) self.tap { @context.merge!(fields) } end # Create a new log with more context. def with(**fields) self.class.new \ context: Yog.merge(@context, fields), generator: @generator, output: @output end ### Helpers for the default log # The log for Yog(), Yog.push(), and Yog.with(). def self.current logs.first end # A stack of receivers with the current 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 current log. def self.push(**fields) current.push(fields) end # Create a new log by extending the context of the current log. def self.with(**fields) current.with(fields) end # Write a line to the current log. def self.write(msg, **fields, &block) current.write(msg, fields, &block) end # Log an error to the current log. def self.error(ex, **fields) current.error(ex, fields) end # Internal: Combine a list of contexts. def self.merge(*contexts) contexts.inject { |a, b| a.merge(b) } end end # Write a line to the current log. def Yog(msg, **fields, &block) case msg when Exception Yog.error(msg, fields) else Yog.write(msg, fields, &block) end end