mirror of
https://github.com/meineerde/rackstash.git
synced 2025-10-17 14:01:01 +00:00
Add logger adapter to write log events to an external logger
This commit is contained in:
parent
0941b24b63
commit
d157e53129
@ -141,5 +141,6 @@ require 'rackstash/logger'
|
||||
|
||||
require 'rackstash/adapters/callable'
|
||||
require 'rackstash/adapters/file'
|
||||
require 'rackstash/adapters/logger'
|
||||
require 'rackstash/adapters/io'
|
||||
require 'rackstash/adapters/null'
|
||||
|
||||
118
lib/rackstash/adapters/logger.rb
Normal file
118
lib/rackstash/adapters/logger.rb
Normal file
@ -0,0 +1,118 @@
|
||||
# frozen_string_literal: true
|
||||
#
|
||||
# Copyright 2017 Holger Just
|
||||
#
|
||||
# This software may be modified and distributed under the terms
|
||||
# of the MIT license. See the LICENSE.txt file for details.
|
||||
|
||||
require 'logger'
|
||||
|
||||
require 'rackstash/adapters/adapter'
|
||||
|
||||
module Rackstash
|
||||
module Adapters
|
||||
# The Logger adapter can be used to write formatted logs to an existing
|
||||
# logger. This is especially useful with libraries exposing a
|
||||
# logger-compatible interface for an external protocol. Example of such
|
||||
# loggers include Ruby's `Syslog::Logger` class, a `Loglier` logger to log
|
||||
# to Loggly or a fluentd logger.
|
||||
#
|
||||
# The only expectation to the passed logger instance is that it responds to
|
||||
# the `add` method with the same semantics as the `Logger` class in Ruby's
|
||||
# standard library. All logs emitted to the given logger will be emitted
|
||||
# with the defined severity (`INFO` by default). Since a log event in
|
||||
# Rackstash can contain multiple concatenanted messages, you should make
|
||||
# sure to format them properly with {Filters} or a custom encoder if
|
||||
# required.
|
||||
#
|
||||
# While most loggers expect Strings as arguments to their `add` method, some
|
||||
# also work with hashes or similar data structures. Make sure to configure a
|
||||
# suitable `encoder` in the responsible {Flow}. By default, we use a JSON
|
||||
# encoder.
|
||||
#
|
||||
# @note When logging to a local file or to an IO object (like `STDOUT` or
|
||||
# `STDERR`), you should use the {File} encoder respectively the {IO} encoder
|
||||
# instead which usally provide stronger consistency guarantees and are
|
||||
# faster.
|
||||
class Logger < Adapter
|
||||
register_for ::Logger, 'Syslog::Logger'
|
||||
|
||||
# @param logger [#add] A base logger to send log lines to. We only expect
|
||||
# this object to implement an `add` method which behaves similar to the
|
||||
# one of the Ruby standard library `Logger` class.
|
||||
# @param severity [Integer, String, Symbol] the severity of the logs
|
||||
# emitted to the base `logger`. It can be specified as either one of the
|
||||
# {SEVERITIES} or a `String` or `Symbol` describing the severity.
|
||||
def initialize(logger, severity: INFO)
|
||||
if logger.respond_to?(:add)
|
||||
@logger = logger
|
||||
else
|
||||
raise TypeError, "#{logger.inspect} does not look like a logger"
|
||||
end
|
||||
|
||||
self.severity = severity
|
||||
end
|
||||
|
||||
# @return [Integer] the severity which will be used to add log events to
|
||||
# the base logger.
|
||||
def severity
|
||||
@severity
|
||||
end
|
||||
|
||||
# This attribute sets the severity of the logs emitted to the base logger.
|
||||
# It can be specified as either one of the {SEVERITIES} or a `String` or
|
||||
# `Symbol` describing the severity (i.e. its name).
|
||||
#
|
||||
# @param severity [Integer, String, Symbol] the severity of the logs
|
||||
# emitted to the base `logger`. It can be specified as either one of the
|
||||
# {SEVERITIES} or a `String` or `Symbol` describing the severity.
|
||||
# @raise [ArgumentError] if no severity could be found for the given
|
||||
# value.
|
||||
def severity=(severity)
|
||||
if severity.is_a?(Integer)
|
||||
@severity = severity
|
||||
else
|
||||
@severity = SEVERITY_NAMES.fetch(severity.to_s.downcase) do
|
||||
raise ArgumentError, "invalid log severity: #{severity.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Close the base logger (if supported). The exact behavior is dependent on
|
||||
# the given logger.
|
||||
#
|
||||
# Usually, no further writes are possible after closing. Further attempts
|
||||
# to {#write} will usually result in an exception being thrown.
|
||||
#
|
||||
# @return [nil]
|
||||
def close
|
||||
@logger.close if @logger.respond_to?(:close)
|
||||
nil
|
||||
end
|
||||
|
||||
# Reopen the base logger (if supported). The exact behavior is dependent
|
||||
# on the given logger.
|
||||
#
|
||||
# @return [nil]
|
||||
def reopen
|
||||
@logger.reopen if @logger.respond_to?(:reopen)
|
||||
nil
|
||||
end
|
||||
|
||||
# Emit a single log line to the base logger with the configured log
|
||||
# {#severity}. If the `Encoder` of the responsible {Flow} created a
|
||||
# `String` object, we will log it to the logger with a trailing newline
|
||||
# removed. Other objects like a `Hash` are passed along unchanged.
|
||||
#
|
||||
# @param log [#to_s] the encoded log event. Most loggers expect a `String`
|
||||
# here. Be sure to use a compatible encoder in the responsible {Flow}.
|
||||
# @return [nil]
|
||||
def write_single(log)
|
||||
log = log.chomp("\n".freeze) if log.is_a?(String)
|
||||
|
||||
@logger.add(@severity, log)
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
118
spec/rackstash/adapters/logger_spec.rb
Normal file
118
spec/rackstash/adapters/logger_spec.rb
Normal file
@ -0,0 +1,118 @@
|
||||
# frozen_string_literal: true
|
||||
#
|
||||
# Copyright 2017 Holger Just
|
||||
#
|
||||
# This software may be modified and distributed under the terms
|
||||
# of the MIT license. See the LICENSE.txt file for details.
|
||||
|
||||
require 'spec_helper'
|
||||
require 'stringio'
|
||||
|
||||
require 'rackstash/adapters/logger'
|
||||
|
||||
describe Rackstash::Adapters::Logger do
|
||||
let(:bucket) {
|
||||
Struct.new(:lines) do
|
||||
def write(log)
|
||||
raise IOError if @closed
|
||||
lines << log
|
||||
end
|
||||
|
||||
def close
|
||||
@closed = true
|
||||
end
|
||||
|
||||
def closed?
|
||||
@closed
|
||||
end
|
||||
end.new([])
|
||||
}
|
||||
let(:logger) {
|
||||
::Logger.new(bucket).tap do |logger|
|
||||
logger.formatter = ->(_severity, _time, _progname, msg) { msg }
|
||||
|
||||
# mock the reopen method on this logger
|
||||
def logger.reopen
|
||||
end
|
||||
end
|
||||
}
|
||||
let(:logger_ducky) {
|
||||
Object.new.tap do |duck|
|
||||
allow(duck).to receive(:add)
|
||||
end
|
||||
}
|
||||
|
||||
let(:adapter) { described_class.new(logger) }
|
||||
|
||||
describe '#initialize' do
|
||||
it 'accepts a Logger object' do
|
||||
expect { described_class.new(logger) }.not_to raise_error
|
||||
expect { described_class.new(logger_ducky) }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'rejects non-logger objects' do
|
||||
expect { described_class.new(nil) }.to raise_error TypeError
|
||||
expect { described_class.new('hello') }.to raise_error TypeError
|
||||
expect { described_class.new(Object.new) }.to raise_error TypeError
|
||||
end
|
||||
end
|
||||
|
||||
describe '.default_encoder' do
|
||||
it 'returns a JSON encoder' do
|
||||
expect(adapter.default_encoder).to be_instance_of Rackstash::Encoders::JSON
|
||||
end
|
||||
end
|
||||
|
||||
describe '#close' do
|
||||
context 'with logger' do
|
||||
it 'closes the logger object' do
|
||||
expect(bucket).not_to be_closed
|
||||
expect(logger).to receive(:close).and_call_original
|
||||
adapter.close
|
||||
expect(bucket).to be_closed
|
||||
end
|
||||
end
|
||||
|
||||
context 'with logger_ducky' do
|
||||
let(:logger) { logger_ducky }
|
||||
|
||||
it 'ignores the call if unsupported' do
|
||||
expect { adapter.close }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#reopen' do
|
||||
context 'with logger' do
|
||||
it 'closes the logger object' do
|
||||
expect(logger).to receive(:reopen).and_call_original
|
||||
adapter.reopen
|
||||
end
|
||||
end
|
||||
|
||||
context 'with logger_ducky' do
|
||||
let(:logger) { logger_ducky }
|
||||
|
||||
it 'ignores the call if unsupported' do
|
||||
expect { adapter.reopen }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#write_single' do
|
||||
it 'writes the log line to the logger object' do
|
||||
adapter.write('a log line')
|
||||
expect(bucket.lines.last).to eql 'a log line'
|
||||
end
|
||||
|
||||
it 'passes the raw object to the logger' do
|
||||
adapter.write([123, 'hello'])
|
||||
expect(bucket.lines.last).to eql [123, 'hello']
|
||||
end
|
||||
|
||||
it 'removes a trailing newline if present' do
|
||||
adapter.write("a full line.\n")
|
||||
expect(bucket.lines.last).to eql 'a full line.'
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
x
Reference in New Issue
Block a user