lib/semantic_logger/formatters/loki.rb
require "json" module SemanticLogger module Formatters class Loki < Base attr_accessor :stream, :payload_value # Returns [String] a single JSON log def call(log, logger) self.logger = logger self.log = log {streams: [build_stream]}.to_json end # Returns [String] a JSON batch of logs def batch(logs, logger) self.logger = logger streams = logs.map do |log| self.log = log build_stream end {streams: streams}.to_json end private def build_stream self.stream = {stream: {pid: pid}, values: [[]]} application environment host level thread tags named_tags context time message payload metric duration exception stream[:values][0] << payload_value stream end def host stream[:stream][:host] = logger.host if log_host && logger.host.to_s end def application stream[:stream][:application] = logger.application if log_application && logger&.application end def environment stream[:stream][:environment] = logger.environment if log_environment && logger&.environment end def level stream[:stream][:level] = log.level end def thread stream[:stream][:thread] = log.thread_name if log.thread_name end def tags stream[:stream][:tags] = log.tags if log.tags.respond_to?(:empty?) && !log.tags.empty? end def named_tags stream[:stream].merge!(log.named_tags) if log.named_tags.respond_to?(:empty?) && !log.named_tags.empty? end def context return unless log.context && !log.context.empty? log.context.each do |key, value| serialized_value = if value.is_a?(Hash) value.to_json else value.to_s end stream[:stream].merge!(key.to_s => serialized_value) end end def time stream[:values][0] << format_time(log) end def message stream[:values][0] << (log.message ? log.cleansed_message : "") end def format_time(log) log.time.strftime("%s%N") end def payload self.payload_value = if log.payload.respond_to?(:empty?) && !log.payload.empty? # Loki only accepts strings as key and values stringify_hash(log.payload) else {} end end def metric return unless log.metric payload_value[:metric] = log.metric payload_value[:metric_value] = log.metric_amount end def duration return unless log.duration payload_value[:duration] = log.duration.to_s payload_value[:duration_human] = log.duration_human end def exception return unless log.exception payload_value.merge!( exception_name: log.exception.class.name, exception_message: log.exception.message, stack_trace: log.exception.backtrace.to_s ) end def stringify_hash(hash) result = {} hash.each do |key, value| string_key = key.to_s result[string_key] = case value when Hash JSON.generate(stringify_hash(value)) else value.to_s end end result end end end end