Skip to content

Commit

Permalink
Use syslog if log_output is not set
Browse files Browse the repository at this point in the history
  • Loading branch information
lzap committed Jan 4, 2016
1 parent d65c55a commit 5b7e862
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 18 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1,3 +1,4 @@
html/*
Gemfile.lock
coverage
myserver*.rb
1 change: 1 addition & 0 deletions daemons.gemspec
Expand Up @@ -24,6 +24,7 @@ Gem::Specification.new do |s|

s.files = `git ls-files README.md LICENSE Releases lib examples`.split

s.add_development_dependency 'rake'
s.add_development_dependency 'rspec', '~> 3.1'
s.add_development_dependency 'simplecov'
s.add_development_dependency 'pry-byebug'
Expand Down
2 changes: 1 addition & 1 deletion examples/run/ctrl_optionparser.rb
Expand Up @@ -28,7 +28,7 @@ def initialize(args)
def run
Daemons.run_proc('myapp', :ARGV => @args, :ontop => !@options.daemonize) do
puts "@options.daemonize: #{@options.daemonize}"
STDOUT.sync = true
$stdout.sync = true
loop do
print '.'
sleep(2)
Expand Down
2 changes: 2 additions & 0 deletions lib/daemons.rb
Expand Up @@ -113,6 +113,7 @@ module Daemons
# <tt>:logfilename</tt>:: Specifiy a custom log file name
# <tt>:log_output</tt>:: When given (i.e. set to true), redirect both STDOUT and STDERR to a logfile named '[app_name].output' (or as given in :output_logfilename) in the pid-file directory
# <tt>:output_logfilename</tt>:: Specifiy a custom output redirection file name
# <tt>:log_output_syslog</tt>:: When set to true, redirect output into SYSLOG instead of the file. This overrides log_output setting.
# <tt>:keep_pid_files</tt>:: When given do not delete lingering pid-files (files for which the process is no longer running).
# <tt>:hard_exit</tt>:: When given use exit! to end a daemons instead of exit (this will for example
# not call at_exit handlers).
Expand Down Expand Up @@ -274,6 +275,7 @@ def call(options = {}, &block)
# <tt>:log_dir</tt>:: A specific directory to put the log files into (when not given, resort to the default
# location as derived from the :dir_mode and :dir options
# <tt>:log_output</tt>:: When given (i.e. set to true), redirect both STDOUT and STDERR to a logfile named '[app_name].output' in the pid-file directory
# <tt>:log_output_syslog</tt>:: When set to true, redirect output into SYSLOG instead of the file. This overrides log_output setting.
# -----
#
# === Example:
Expand Down
16 changes: 11 additions & 5 deletions lib/daemons/application.rb
Expand Up @@ -78,7 +78,9 @@ def output_logfilename
end

def output_logfile
if log_output?
if log_output_syslog?
'SYSLOG'
elsif log_output?
File.join logdir, output_logfilename
end
end
Expand Down Expand Up @@ -378,7 +380,7 @@ def stop(no_wait = false)
unless no_wait
if @force_kill_waittime > 0
@report.stopping_process(group.app_name, pid)
STDOUT.flush
$stdout.flush

begin
Timeout.timeout(@force_kill_waittime, TimeoutError) do
Expand All @@ -388,7 +390,7 @@ def stop(no_wait = false)
end
rescue TimeoutError
@report.forcefully_stopping_process(group.app_name, pid)
STDOUT.flush
$stdout.flush

begin
Process.kill('KILL', pid)
Expand All @@ -403,7 +405,7 @@ def stop(no_wait = false)
end
rescue TimeoutError
@report.cannot_stop_process(group.app_name, pid)
STDOUT.flush
$stdout.flush
end
end
end
Expand All @@ -417,7 +419,7 @@ def stop(no_wait = false)
begin; @pid.cleanup; rescue ::Exception; end

@report.stopped_process(group.app_name, pid)
STDOUT.flush
$stdout.flush
end
end

Expand Down Expand Up @@ -456,6 +458,10 @@ def log_output?
options[:log_output] && logdir
end

def log_output_syslog?
options[:log_output_syslog]
end

def dir_mode
@dir_mode or group.dir_mode
end
Expand Down
33 changes: 21 additions & 12 deletions lib/daemons/daemonize.rb
Expand Up @@ -30,7 +30,7 @@ def simulate(logfile_name = nil, app_name = nil)
close_io

# Free STDIN and point it to somewhere sensible
begin; STDIN.reopen '/dev/null'; rescue ::Exception; end
begin; $stdin.reopen '/dev/null'; rescue ::Exception; end

# Split rand streams between spawning and daemonized process
srand
Expand Down Expand Up @@ -122,7 +122,7 @@ def close_io
# Make sure all input/output streams are closed
# Part I: close all IO objects (except for STDIN/STDOUT/STDERR)
ObjectSpace.each_object(IO) do |io|
unless [STDIN, STDOUT, STDERR].include?(io)
unless [$stdin, $stdout, $stderr].include?(io)
io.close rescue nil
end
end
Expand All @@ -138,22 +138,31 @@ def close_io
# Free STDIN/STDOUT/STDERR file descriptors and
# point them somewhere sensible
def redirect_io(logfile_name)
begin; STDIN.reopen '/dev/null'; rescue ::Exception; end
begin; $stdin.reopen '/dev/null'; rescue ::Exception; end

if logfile_name
if logfile_name == 'SYSLOG'
# attempt to use syslog via syslogio
begin
STDOUT.reopen logfile_name, 'a'
File.chmod(0644, logfile_name)
STDOUT.sync = true
require 'syslogio'
$stdout = ::Daemons::SyslogIO.new($0, :local0, :info, $stdout)
$stderr = ::Daemons::SyslogIO.new($0, :local0, :err, $stderr)
# error out early so we can fallback to null
$stdout.puts "no logfile provided, output redirected to syslog"
rescue ::Exception
begin; STDOUT.reopen '/dev/null'; rescue ::Exception; end
# on unsupported platforms simply reopen /dev/null
begin; $stdout.reopen '/dev/null'; rescue ::Exception; end
begin; $stderr.reopen '/dev/null'; rescue ::Exception; end
end
elsif logfile_name
$stdout.reopen logfile_name, 'a'
File.chmod(0644, logfile_name)
$stdout.sync = true
begin; $stderr.reopen $stdout; rescue ::Exception; end
$stderr.sync = true
else
begin; STDOUT.reopen '/dev/null'; rescue ::Exception; end
begin; $stdout.reopen '/dev/null'; rescue ::Exception; end
begin; $stderr.reopen '/dev/null'; rescue ::Exception; end
end

begin; STDERR.reopen STDOUT; rescue ::Exception; end
STDERR.sync = true
end
module_function :redirect_io
end
240 changes: 240 additions & 0 deletions lib/daemons/syslogio.rb
@@ -0,0 +1,240 @@
# This is a simple class meant to allow using syslog through an IO-like object. Code
# borrowed from https://github.com/phemmer/ruby-syslogio
#
# The usage is simple:
#
# require 'syslogio'
# $stdout = SyslogIO.new("myapp", :local0, :info, $stdout)
# $stderr = SyslogIO.new("myapp", :local0, :err, $stderr)
# $stdout.puts "This is a message"
# $stderr.puts "This is an error"
# raise StandardError, 'This will get written through the SyslogIO for $stderr'

class Daemons::SyslogIO
require 'syslog'

# Indicates whether synchonous IO is enabled.
# @return [Boolean]
attr_reader :sync

# @!visibility private
def self.syslog_constant_sym(option)
return unless option.is_a?(Symbol) or option.is_a?(String)
option = option.to_s.upcase
option = "LOG_#{option}" unless option[0..4] == 'LOG_'
option = option.to_sym
option
end
# @!visibility private
def self.syslog_constant(option)
return unless option = syslog_constant_sym(option)
return Syslog.constants.include?(option) ? Syslog.const_get(option) : nil
end
# @!visibility private
def self.syslog_facility(option)
return unless option = syslog_constant_sym(option)
return Syslog::Facility.constants.include?(option) ? Syslog.const_get(option) : nil
end
# @!visibility private
def self.syslog_level(option)
return unless option = syslog_constant_sym(option)
return Syslog::Level.constants.include?(option) ? Syslog.const_get(option) : nil
end
# @!visibility private
def self.syslog_option(option)
return unless option = syslog_constant_sym(option)
return Syslog::Option.constants.include?(option) ? Syslog.const_get(option) : nil
end

# Creates a new object.
# You can have as many SyslogIO objects as you like. However because they all share the same syslog connection, some parameters are shared. The identifier shared among all SyslogIO objects, and is set to the value of the last one created. The Syslog options are merged together as a combination of all objects. The facility and level are distinct between each though.
# If an IO object is provided as an argument, any text written to the SyslogIO object will also be passed through to that IO object.
#
# @param identifier [String] Identifier
# @param facility [Fixnum<Syslog::Facility>] Syslog facility
# @param level [Fixnum<Syslog::Level>] Syslog level
# @param option [Fixnum<Syslog::Options>] Syslog option
# @param passthrough [IO] IO passthrough
def initialize(*options)
options.each do |option|
if option.is_a?(String)
@ident = option
elsif value = self.class.syslog_facility(option)
@facility = value
elsif value = self.class.syslog_level(option)
@level = value
elsif value = self.class.syslog_option(option)
@options = 0 if @options.nil?
@options |= value
elsif option.is_a?(IO)
@out = option
else
raise ArgumentError, "Unknown argument #{option.inspect}"
end
end

@options ||= 0
@ident ||= $0.sub(/.*\//, '')
@facility ||= Syslog::LOG_USER
@level ||= Syslog::LOG_INFO

if Syslog.opened? then
options = Syslog.options | @options
@syslog = Syslog.reopen(@ident, options, @facility)
else
@syslog = Syslog.open(@ident, @options, @facility)
end

@subs = []
@sync = false
@buffer = ''

at_exit { flush }
end

# Add a substitution rule
#
# These substitutions will be applied to each line before it is logged. This can be useful if some other gem is generating log content and you want to change the formatting.
# @param regex [Regex]
def sub_add(regex, replacement)
@subs << [regex, replacement]
end

# Enable or disable synchronous IO (buffering).
#
# When false (default), output will be line buffered. For syslog this is optimal so the log entries are complete lines.
def sync=(sync)
if sync != true and sync != false then
raise ArgumentError, "sync must be true or false"
end
@sync = sync
if sync == true then
flush
end
end

# Write to syslog respecting the behavior of the {#sync} setting.
def write(text)
if @sync then
syswrite(text)
else
text.split(/(\n)/).each do |line|
@buffer = @buffer + line.to_s
if line == "\n" then
flush
end
end
end
end
alias_method :<<, :write

# Write to syslog directly, bypassing buffering if enabled.
def syswrite(text)
begin
@out.syswrite(text) if @out and !@out.closed?
rescue SystemCallError => e
end

text.split(/\n/).each do |line|
@subs.each do |sub|
line.sub!(sub[0], sub[1])
end
if line == '' or line.match(/^\s*$/) then
next
end
Syslog.log(@facility | @level, line)
end
nil
end

# Immediately flush any buffered data
def flush
syswrite(@buffer)
@buffer = ''
end

# Log at the debug level
#
# Shorthand for {#log}(text, Syslog::LOG_DEBUG)
def debug(text)
log(text, Syslog::LOG_DEBUG)
end

# Log at the info level
#
# Shorthand for {#log}(text, Syslog::LOG_INFO)
def info(text)
log(text, Syslog::LOG_INFO)
end

# Log at the notice level
#
# Shorthand for {#log}(text, Syslog::LOG_NOTICE)
def notice(text)
log(text, Syslog::LOG_NOTICE)
end
alias_method :notify, :notice

# Log at the warning level
#
# Shorthand for {#log}(text, Syslog::LOG_WARNING)
def warn(text)
log(text, Syslog::LOG_WARNING)
end

# Log at the error level
#
# Shorthand for {#log}(text, Syslog::LOG_ERR)
def error(text)
log(text, Syslog::LOG_ERR)
end

# Log at the critical level
#
# Shorthand for {#log}(text, Syslog::LOG_CRIT)
def crit(text)
log(text, Syslog::LOG_CRIT)
end
alias_method :fatal, :crit

# Log at the emergency level
#
# Shorthand for {#log}(text, Syslog::LOG_EMERG)
def emerg(text)
log(text, Syslog::LOG_EMERG)
end

# Log a complete line
#
# Similar to {#write} but appends a newline if not present.
def puts(*texts)
texts.each do |text|
write(text.chomp + "\n")
end
end

# Write a complete line at the specified log level
#
# Similar to {#puts} but allows changing the log level for just this one message
def log(text, level = nil)
if priority.nil? then
write(text.chomp + "\n")
else
priority_bkup = @priority
#TODO fix this to be less ugly. Temporarily setting an instance variable is evil
@priority = priority
write(text.chomp + "\n")
@priority = priority_bkup
end
end

# @!visibility private
def noop(*args)
end
alias_method :reopen, :noop

# false
def isatty
false
end
end

0 comments on commit 5b7e862

Please sign in to comment.