lib/loggerstash.rb in loggerstash-0.0.1 vs lib/loggerstash.rb in loggerstash-0.0.2

- old
+ new

@@ -12,20 +12,55 @@ # 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 - def initialize(logstash_server:, metrics_registry: nil, formatter: nil) + # 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. + # + def initialize(logstash_server:, metrics_registry: nil, formatter: nil, logstash_writer: nil) @logstash_server = logstash_server @metrics_registry = metrics_registry @formatter = formatter + @logstash_writer = logstash_writer @op_mutex = Mutex.new end + # Associate this Loggerstash with a Logger (or class of Loggers). + # + # A single Loggerstash instance can be associated with one or more Logger + # objects, or all instances of Logger, by attaching the Loggerstash to the + # other object (or class). Attaching a Loggerstash means it can no longer + # be configured (by the setter methods). + # + # @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) @op_mutex.synchronize do obj.instance_variable_set(:@loggerstash, self) if obj.is_a?(Module) @@ -48,25 +83,40 @@ 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 if @logstash_writer.nil? {}.tap do |opts| opts[:server_name] = @logstash_server @@ -78,10 +128,13 @@ @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 { "@timestamp" => t.utc.strftime("%FT%T.%NZ"), message: m, @@ -90,10 +143,12 @@ ev[:progname] = p if p end end end + # The methods needed to turn any Logger into a Loggerstash Logger. + # module Mixin private # Hooking into this specific method may seem... unorthodox, but # it seemingly has an extremely stable interface and is the most @@ -102,14 +157,23 @@ loggerstash.log_message(s, t, p, m) super end + # Find where our associated Loggerstash object is being held captive. + # + # 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| if ls.nil? + #:nocov: raise RuntimeError, "Cannot find loggerstash instance. CAN'T HAPPEN." + #:nocov: end end end end end