diff --git a/lib/rackstash.rb b/lib/rackstash.rb index e183c18..53965cb 100644 --- a/lib/rackstash.rb +++ b/lib/rackstash.rb @@ -54,3 +54,5 @@ module Rackstash end require 'rackstash/logger' + +require 'rackstash/adapters/io' diff --git a/lib/rackstash/adapters.rb b/lib/rackstash/adapters.rb index 5abbb3b..3d13978 100644 --- a/lib/rackstash/adapters.rb +++ b/lib/rackstash/adapters.rb @@ -143,3 +143,4 @@ module Rackstash end end end + diff --git a/lib/rackstash/adapters/io.rb b/lib/rackstash/adapters/io.rb new file mode 100644 index 0000000..a1ce80d --- /dev/null +++ b/lib/rackstash/adapters/io.rb @@ -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 diff --git a/spec/rackstash/adapters/io_spec.rb b/spec/rackstash/adapters/io_spec.rb new file mode 100644 index 0000000..f77f220 --- /dev/null +++ b/spec/rackstash/adapters/io_spec.rb @@ -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