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