1
0
mirror of https://github.com/meineerde/rackstash.git synced 2025-12-19 15:01:12 +00:00

Add logger adapter to write log events to an external logger

This commit is contained in:
Holger Just 2017-10-03 20:25:30 +02:00
parent 0941b24b63
commit d157e53129
3 changed files with 237 additions and 0 deletions

View File

@ -141,5 +141,6 @@ require 'rackstash/logger'
require 'rackstash/adapters/callable' require 'rackstash/adapters/callable'
require 'rackstash/adapters/file' require 'rackstash/adapters/file'
require 'rackstash/adapters/logger'
require 'rackstash/adapters/io' require 'rackstash/adapters/io'
require 'rackstash/adapters/null' require 'rackstash/adapters/null'

View 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

View 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