1
0
mirror of https://github.com/meineerde/rackstash.git synced 2025-10-17 14:01:01 +00:00

Add IO adapter

This adapter allows to write newline-separated log lines to an existing
IO object, e.g. STDOUT. It does not allow to reopen the IO device.
This commit is contained in:
Holger Just 2017-06-06 23:06:30 +02:00
parent b3f9a63253
commit 00786283f0
4 changed files with 147 additions and 0 deletions

View File

@ -54,3 +54,5 @@ module Rackstash
end
require 'rackstash/logger'
require 'rackstash/adapters/io'

View File

@ -143,3 +143,4 @@ module Rackstash
end
end
end

View File

@ -0,0 +1,77 @@
# 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 'thread'
require 'rackstash/adapters/adapter'
module Rackstash
module Adapters
# This adapter allows to write logs to an existing `IO` object, e.g.,
# `STDOUT`, an open file, a `StringIO` object, ...
#
# When writing a [12factor](https://12factor.net/logs) app, you can use this
# adapter to write formatted logs to `STDOUT` of the process to be captured
# by the environment and eventually sent to a log collector.
#
# Concurrent writes to this adapter will be serialized to ensure there are
# no overlapping writes. You still have to ensure that there are no other
# writes to the IO object from outside this adapter to ensure there that
# is no overlapping data visible on the IO object.
#
# Note that with some deployment models involving pre-forked application
# servers, e.g., Unicorn or Puma servers with multiple worker processes, the
# combined `STDOUT` stream of multiple processes can cause interleaved data
# when writing large log lines (typically > 4 KB). If you are using such a
# deployment model and expect large log lines, you should consider using a
# different adapter to ensure consistent logs.
#
# Suitable adapters include:
#
# * {Rackstash::Adapters::File} - When writing to a file, we ensure with
# explicit file locks that all data is written consistently.
# * {Rackstash::Adapters::TCP} - With a single TCP connection per adapter
# instance, the receiver can handle the log lines separately.
class IO < Adapter
register_for ->(o) { o.respond_to?(:write) && o.respond_to?(:close) }
# @param io [#write, #close] an IO object. It must at least respond to
# `write` and `close`.
def initialize(io)
unless io.respond_to?(:write) && io.respond_to?(:close)
raise TypeError, "#{io.inspect} does not look like an IO object"
end
@io = io
@mutex = Mutex.new
end
# Write a single log line with a trailing newline character to the IO
# object.
#
# @param log [#to_s] the encoded log event
# @return [nil]
def write_single(log)
@mutex.synchronize do
@io.write normalize_line(log)
end
nil
end
# Close the IO object.
#
# After closing, no further writes are possible. Further attempts to
# {#write} will result in an exception being thrown.
#
# @return [nil]
def close
@mutex.synchronize do
@io.close
end
nil
end
end
end
end

View File

@ -0,0 +1,67 @@
# 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 'tempfile'
require 'rackstash/adapters/io'
describe Rackstash::Adapters::IO do
let(:io) { StringIO.new }
let(:adapter) { Rackstash::Adapters::IO.new(io) }
describe '#initialize' do
it 'accepts an IO object' do
expect { Rackstash::Adapters::IO.new($stdout) }.not_to raise_error
expect { Rackstash::Adapters::IO.new(StringIO.new) }.not_to raise_error
expect { Rackstash::Adapters::IO.new(Tempfile.new('foo')) }.not_to raise_error
end
it 'rejects non-IO objects' do
expect { Rackstash::Adapters::IO.new(nil) }.to raise_error TypeError
expect { Rackstash::Adapters::IO.new('hello') }.to raise_error TypeError
expect { Rackstash::Adapters::IO.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
it 'closes the IO object' do
expect(io).to receive(:close).and_call_original
adapter.close
expect { adapter.write('hello') }.to raise_error IOError
end
end
describe '#reopen' do
it 'does nothing' do
expect(io).not_to receive(:close)
adapter.reopen
end
end
describe '#write_single' do
it 'writes the log line to the IO object' do
adapter.write('a log line')
expect(io.tap(&:rewind).read).to eql "a log line\n"
end
it 'always writes a string' do
adapter.write([123, 'hello'])
expect(io.tap(&:rewind).read).to eql "[123, \"hello\"]\n"
end
it 'appends a trailing newline if necessary' do
adapter.write("a full line.\n")
expect(io.tap(&:rewind).read).to eql "a full line.\n"
end
end
end