# ----------------------------------------------------------------------------- # # Sawmill log rotation utility # # ----------------------------------------------------------------------------- # Copyright 2009 Daniel Azuma # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # * Neither the name of the copyright holder, nor the names of any other # contributors to this software, may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # ----------------------------------------------------------------------------- ; require 'thread' module Sawmill # The Sawmill Rotater provides log rotation services for logfile # formatting, supporting several different log rotation strategies. # # The formatter implemented by Sawmill::EntryProcessor::Format already # recognizes a Rotater as a supported destination, and automatically # interfaces with it to ensure that log records aren't split into # multiple files by rotation. # # You may also interface with a rotater manually. The core of rotater # usage is the Channel, which lets you ensure that groups of log # entries end up in the same log file regardless of log rotation. # Generally, to use a rotater, you obtain one or more channels and # write formatted entries to them, either telling them explicitly # where the allowable file breaks are, or by letting the rotater # break the file anywhere it wants. See the create_channel method # and the Channel object for more details. class Rotater # :stopdoc: SUPPORTS_ENCODING = defined?(::Encoding) ENCODING_OPTS = {:invalid => :replace, :undef => :replace} # :startdoc: # Create a rotater using the given rotation strategy. # See Sawmill::Rotater::DateBasedLogFile and # Sawmill::Rotater::ShiftingLogFile for examples of common strategies. # # The rotation strategy can be passed as an object or as a class with a # set of options that will be used to instantiate the strategy. # In addition to those options, the following options are recognized: # # :omit_directives:: # If true, omit standard logfile directives. Default is false. # :concurrent_writes:: # Set this to true if you expect multiple processes to attempt to # write to the same log file simultaneously. This option causes the # rotater to surround writes with an acquisition of the cooperative # filesystem lock (if available) for the logfile, in an attempt to # prevent lines from interleaving in one another. Default is false. # :encoding:: # Specify an encoding for file data. (Ruby 1.9 only). # You may pass either an encoding object or an encoding name. # If not specified, writes raw bytes (e.g. defaults to ASCII-8BIT). def initialize(io_manager_, opts_={}) @omit_directives = opts_.delete(:omit_directives) @concurrent_writes = opts_.delete(:concurrent_writes) if SUPPORTS_ENCODING @encoding = opts_.delete(:encoding) if @encoding && !@encoding.respond_to?(:name) @encoding = ::Encoding.find(@encoding) end end if io_manager_.kind_of?(::Class) @io_manager = io_manager_.new(opts_) else @io_manager = io_manager_ end @handles ||= {} @mutex ||= ::Monitor.new end # Create a new Channel for this Rotater. See Sawmill::Rotater::Channel # for details on the Channel object. # # The following options are recognized: # # :auto_rotate:: # Put the channel in auto-rotate mode. In this mode, the rotater is # allowed to rotate the logfile at any time for that channel. It is # the equivalent of calling check_rotate on the channel after every # write. Default is false. def create_channel(opts_={}) Channel.new(self, opts_) end def _write_to_stream(io_, str_) if SUPPORTS_ENCODING && @encoding str_ = str_.encode(@encoding, ENCODING_OPTS) end if @concurrent_writes begin io_.flock(::File::LOCK_EX) io_.write(str_) ensure io_.flock(::File::LOCK_UN) end else io_.write(str_) end end def _obtain_handle # :nodoc: handle_ = @io_manager.preferred_handle if @handles.include?(handle_) @handles[handle_][2] += 1 else io_ = @io_manager.open_handle(handle_) unless @omit_directives _write_to_stream(io_, "# sawmill_format: version=1\n") if defined?(::Encoding) encoding_ = io_.encoding if encoding_ _write_to_stream(io_, "# sawmill_format: encoding=#{encoding_.name}\n") end end end @handles[handle_] = [handle_, io_, 1] end handle_ end def _release_handle(handle_) # :nodoc: info_ = @handles[handle_] info_[2] -= 1 if info_[2] == 0 @io_manager.close_handle(handle_, info_[1]) @handles.delete(handle_) end nil end def _check_rotate_handle(handle_) # :nodoc: if handle_ != @io_manager.preferred_handle _release_handle(handle_) _obtain_handle else handle_ end end def _do_open # :nodoc: @mutex.synchronize do _obtain_handle end end def _do_write(handle_, str_, auto_rotate_) # :nodoc: @mutex.synchronize do @io_manager.before_write if auto_rotate_ handle_ = _check_rotate_handle(handle_) end info_ = @handles[handle_] _write_to_stream(info_[1], str_) handle_ end end def _do_close(handle_) # :nodoc: @mutex.synchronize do _release_handle(handle_) end end def _do_check_rotate(handle_) # :nodoc: @mutex.synchronize do _check_rotate_handle(handle_) end end # A channel is a lightweight object that responds to the write and close # methods; that is, it is sufficient for Sawmill::Formatter. # # When a channel is opened, it locks down a path to the logfile and # ensures that the logfile will not rotate out from under it; that is, # writes to a channel are ensured to end up in the same physical file. # # You may choose, at intervals, to explicitly tell the channel that it # is okay to rotate the logfile now, by calling check_rotate. # # You must close a channel when you are done with it. Closing a channel # does not close the underlying logfile, but instead tells the rotater # that you are done with this channel and that the logfile is free to # rotate independent of it. # # You may have any number of channels open at any time, each on a # different rotation schedule. Each may possibly be writing to different # files in the rotation at any time, but this is all done automatically # behind the scenes. class Channel def initialize(rotater_, opts_={}) # :nodoc: @rotater = rotater_ @auto_rotate = opts_[:auto_rotate] @io_handle = @rotater._do_open end # Write a string to this channel. def write(str_) if @io_handle @rotater._do_write(@io_handle, str_, @auto_rotate) end end # Close this channel, telling the rotater that this channel no longer # needs to constrain the log rotation. def close if @io_handle @rotater._do_close(@io_handle) @io_handle = nil end end # Manually tell the rotater that this channel is at a stopping point # and that the log file may rotate at this time. def check_rotate if @io_handle @io_handle = @rotater._do_check_rotate(@io_handle) end end end end end