lib/loggerstash.rb in loggerstash-0.0.8 vs lib/loggerstash.rb in loggerstash-0.0.9

- old
+ new

@@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'logstash_writer' require 'thread' # A sidecar class to augment a Logger with super-cow-logstash-forwarding # powers. @@ -12,35 +14,33 @@ # Raised if any configuration setter methods are called (`Loggerstash#<anything>=`) # after the loggerstash instance has been attached to a logger. # class AlreadyRunningError < Error; end - # Set the formatter proc to a new proc. - # - # The passed in proc must take four arguments: `severity`, `timestamp`, - # `progname` and `message`. `timestamp` is a `Time`, all over arguments - # are `String`s, and `progname` can possibly be `nil`. It must return a - # Hash containing the parameters you wish to send to logstash. - # - attr_writer :formatter - # A new Loggerstash! # # @param logstash_server [String] an address:port, hostname:port, or srvname # to which a `json_lines` logstash connection can be made. + # # @param metrics_registry [Prometheus::Client::Registry] where the metrics # which are used by the underlying `LogstashWriter` should be registered, # for later presentation by the Prometheus client. + # # @param formatter [Proc] a formatting proc which takes the same arguments # as the standard `Logger` formatter, but rather than emitting a string, # it should pass back a Hash containing all the fields you wish to send # to logstash. + # # @param logstash_writer [LogstashWriter] in the event that you've already # got a LogstashWriter instance configured, you can pass it in here. Note # that any values you've set for logstash_server and metrics_registry # will be ignored. # + # @param logger [Logger] passed to the LogstashWriter we create. May or + # may not, itself, be attached to the Loggerstash for forwarding to + # logstash (Logception!). + # def initialize(logstash_server:, metrics_registry: nil, formatter: nil, logstash_writer: nil, logger: nil) @logstash_server = logstash_server @metrics_registry = metrics_registry @formatter = formatter @logstash_writer = logstash_writer @@ -59,24 +59,25 @@ # @param obj [Object] the instance or class to attach this Loggerstash to. # We won't check that you're attaching to an object or class that will # benefit from the attachment; that's up to you to ensure. # def attach(obj) + run_writer + @op_mutex.synchronize do - obj.instance_variable_set(:@loggerstash, self) + obj.instance_variable_set(:@logstash_writer, @logstash_writer) + obj.instance_variable_set(:@loggerstash_formatter, @formatter) if obj.is_a?(Module) obj.prepend(Mixin) else obj.singleton_class.prepend(Mixin) end - - run_writer end end - %i{logstash_server metrics_registry}.each do |sym| + %i{formatter logger logstash_server metrics_registry}.each do |sym| define_method(:"#{sym}=") do |v| @op_mutex.synchronize do if @logstash_writer raise AlreadyRunningError, "Cannot change #{sym} once writer is running" @@ -84,120 +85,111 @@ instance_variable_set(:"@#{sym}", v) end end end - # Send a logger message to logstash. - # - # @private - # - def log_message(s, t, p, m) - @op_mutex.synchronize do - if @logstash_writer.nil? - #:nocov: - run_writer - #:nocov: - end - - @logstash_writer.send_event((@formatter || default_formatter).call(s, t, p, m)) - end - end - private # Do the needful to get the writer going. # - # This will error out unless the @op_mutex is held at the time the - # method is called; we can't acquire it ourselves because some calls - # to run_writer already need to hold the mutex. - # def run_writer - unless @op_mutex.owned? - #:nocov: - raise RuntimeError, - "Must call run_writer while holding @op_mutex" - #:nocov: - end + @op_mutex.synchronize do + if @logstash_writer.nil? + {}.tap do |opts| + opts[:server_name] = @logstash_server + if @metrics_registry + opts[:metrics_registry] = @metrics_registry + end + if @logger + opts[:logger] = @logger + end - if @logstash_writer.nil? - {}.tap do |opts| - opts[:server_name] = @logstash_server - if @metrics_registry - opts[:metrics_registry] = @metrics_registry + @logstash_writer = LogstashWriter.new(**opts) + @logstash_writer.start! end - if @logger - opts[:logger] = @logger - end - - @logstash_writer = LogstashWriter.new(**opts) - @logstash_writer.run end end end - # Mangle the standard sev/time/prog/msg set into a minimal logstash - # event. - # - def default_formatter - @default_formatter ||= ->(s, t, p, m) do - caller = caller_locations.find { |loc| ! [__FILE__, logger_filename].include? loc.absolute_path } - - { - "@timestamp": t.utc.strftime("%FT%T.%NZ"), - "@metadata": { event_type: "loggerstash" }, - message: m, - severity_name: s.downcase, - hostname: Socket.gethostname, - pid: $$, - thread_id: Thread.current.object_id, - caller: { - absolute_path: caller.absolute_path, - base_label: caller.base_label, - label: caller.label, - lineno: caller.lineno, - path: caller.path, - }, - }.tap do |ev| - ev[:progname] = p if p - end - end - end - - # Identify the absolute path of the file that defines the Logger class. - # - def logger_filename - @logger_filename ||= Logger.instance_method(:format_message).source_location.first - end - # The methods needed to turn any Logger into a Loggerstash Logger. # module Mixin + attr_writer :logstash_writer, :loggerstash_formatter + private # Hooking into this specific method may seem... unorthodox, but # it seemingly has an extremely stable interface and is the most # appropriate place to inject ourselves. + # def format_message(s, t, p, m) - loggerstash.log_message(s, t, p, m) + loggerstash_log_message(s, t, p, m) super end - # Find where our associated Loggerstash object is being held captive. + # Send a logger message to logstash. # + def loggerstash_log_message(s, t, p, m) + logstash_writer.send_event(loggerstash_formatter.call(s, t, p, m)) + end + + # The current formatter for logstash-destined messages. + # + def loggerstash_formatter + @loggerstash_formatter ||= self.class.ancestors.find { |m| m.instance_variable_defined?(:@loggerstash_formatter) }.instance_variable_get(:@loggerstash_formatter) || default_loggerstash_formatter + end + + # Find the relevant logstash_writer for this Logger. + # # We're kinda reimplementing Ruby's method lookup logic here, but there's # no other way to store our object *somewhere* in the object + class # hierarchy and still be able to get at it from a module (class variables - # don't like being accessed from modules). - # - def loggerstash - ([self] + self.class.ancestors).find { |m| m.instance_variable_defined?(:@loggerstash) }.instance_variable_get(:@loggerstash).tap do |ls| + # don't like being accessed from modules). This is necessary because you + # can attach Loggerstash to the Logger class, not just to an instance. + def logstash_writer + @logstash_writer ||= self.class.ancestors.find { |m| m.instance_variable_defined?(:@logstash_writer) }.instance_variable_get(:@logstash_writer).tap do |ls| if ls.nil? #:nocov: raise RuntimeError, "Cannot find loggerstash instance. CAN'T HAPPEN." #:nocov: end end + end + + # Mangle the standard sev/time/prog/msg set into a logstash + # event. + # + def default_loggerstash_formatter + ->(s, t, p, m) do + caller = caller_locations.find { |loc| ! [__FILE__, logger_filename].include? loc.absolute_path } + + { + "@timestamp": t.utc.strftime("%FT%T.%NZ"), + "@metadata": { event_type: "loggerstash" }, + message: m, + severity_name: s.downcase, + hostname: Socket.gethostname, + pid: $$, + thread_id: Thread.current.object_id, + caller: { + absolute_path: caller.absolute_path, + base_label: caller.base_label, + label: caller.label, + lineno: caller.lineno, + path: caller.path, + }, + }.tap do |ev| + ev[:progname] = p if p + ev[:thread_name] = Thread.current.name if Thread.current.name + end + end + end + + # Identify the absolute path of the file that defines the Logger class. + # + def logger_filename + @logger_filename ||= Logger.instance_method(:format_message).source_location.first end end end