lib/remote_syslog/cli.rb in remote_syslog-1.6.5 vs lib/remote_syslog/cli.rb in remote_syslog-1.6.6.rc1

- old
+ new

@@ -1,199 +1,257 @@ require 'optparse' require 'yaml' require 'pathname' -require 'daemons' +require 'servolux' +require 'remote_syslog/agent' module RemoteSyslog class Cli FIELD_REGEXES = { 'syslog' => /^(\w+ +\d+ \S+) (\S+) ([^: ]+):? (.*)$/, 'rfc3339' => /^(\S+) (\S+) ([^: ]+):? (.*)$/ } + DEFAULT_PID_FILES = [ + "/var/run/remote_syslog.pid", + "#{ENV['HOME']}/run/remote_syslog.pid", + "#{ENV['HOME']}/tmp/remote_syslog.pid", + "#{ENV['HOME']}/remote_syslog.pid", + "#{ENV['TMPDIR']}/remote_syslog.pid", + "/tmp/remote_syslog.pid" + ] + + DEFAULT_CONFIG_FILE = '/etc/log_files.yml' + def self.process!(argv) c = new(argv) c.parse c.run end + attr_reader :program_name + def initialize(argv) @argv = argv + @program_name = File.basename($0) - @app_name = File.basename($0) || 'remote_syslog' - - @configfile = '/etc/log_files.yml' @strip_color = false @exclude_pattern = nil @daemonize_options = { :ARGV => %w(start), :dir_mode => :system, :backtrace => false, :monitor => false, } + + @agent = RemoteSyslog::Agent.new(:pid_file => default_pid_file) end - def pid_file=(v) - m = v.match(%r{^(.+/)?([^/]+?)(\.pid)?$}) - if m[1] - @daemonize_options[:dir_mode] = :normal - @daemonize_options[:dir] = m[1] - end + def is_file_writable?(file) + directory = File.dirname(file) - @app_name = m[2] + (File.directory?(directory) && File.writable?(directory) && !File.exists?(file)) || File.writable?(file) end + def default_pid_file + DEFAULT_PID_FILES.each do |file| + return file if is_file_writable?(file) + end + end + def parse op = OptionParser.new do |opts| - opts.banner = "Usage: remote_syslog [options] [<logfile>...]" + opts.banner = "Usage: #{program_name} [OPTION]... <FILE>..." opts.separator '' - opts.separator "Example: remote_syslog -c configs/logs.yml -p 12345 /var/log/mysqld.log" - opts.separator '' + opts.separator "Options:" opts.on("-c", "--configfile PATH", "Path to config (/etc/log_files.yml)") do |v| - @configfile = File.expand_path(v) + @configfile = v end opts.on("-d", "--dest-host HOSTNAME", "Destination syslog hostname or IP (logs.papertrailapp.com)") do |v| - @dest_host = v + @agent.destination_host = v end opts.on("-p", "--dest-port PORT", "Destination syslog port (514)") do |v| - @dest_port = v + @agent.destination_port = v end opts.on("-D", "--no-detach", "Don't daemonize and detach from the terminal") do @no_detach = true end opts.on("-f", "--facility FACILITY", "Facility (user)") do |v| - @facility = v + @agent.facility = v end opts.on("--hostname HOST", "Local hostname to send from") do |v| - @hostname = v + @agent.hostname = v end - opts.on("-P", "--pid-dir DIRECTORY", "Directory to write .pid file in (/var/run/)") do |v| - @daemonize_options[:dir_mode] = :normal - @daemonize_options[:dir] = v + opts.on("-P", "--pid-dir DIRECTORY", "DEPRECATED: Directory to write .pid file in") do |v| + puts "Warning: --pid-dir is deprecated. Please use --pid-file FILENAME instead" + @pid_directory = v end - opts.on("--pid-file FILENAME", "PID filename (<program name>.pid)") do |v| - self.pid_file = v + opts.on("--pid-file FILENAME", "Location of the PID file (default #{@agent.pid_file})") do |v| + @agent.pid_file = v end opts.on("--parse-syslog", "Parse file as syslog-formatted file") do - @parse_fields = FIELD_REGEXES['syslog'] + @agent.parse_fields = FIELD_REGEXES['syslog'] end opts.on("-s", "--severity SEVERITY", "Severity (notice)") do |v| - @severity = v + @agent.severity = v end opts.on("--strip-color", "Strip color codes") do - @strip_color = true + @agent.strip_color = true end opts.on("--tls", "Connect via TCP with TLS") do - @tls = true + @agent.tls = true end - opts.on_tail("-h", "--help", "Show this message") do + + + opts.on("--new-file-check-interval INTERVAL", OptionParser::DecimalInteger, + "Time between checks for new files") do |v| + @agent.glob_check_interval = v + end + + opts.separator '' + opts.separator 'Advanced options:' + + opts.on("--[no-]eventmachine-tail", "Enable or disable using eventmachine-tail") do |v| + @agent.eventmachine_tail = v + end + opts.on("--debug-log FILE", "Log internal debug messages") do |v| + level = @agent.logger.level + @agent.logger = Logger.new(v) + @agent.logger.level = level + end + + severities = Logger::Severity.constants + Logger::Severity.constants.map { |s| s.downcase } + opts.on("--debug-level LEVEL", severities, "Log internal debug messages at level") do |v| + @agent.logger.level = Logger::Severity.const_get(v.upcase) + end + + opts.separator "" + opts.separator "Common options:" + + opts.on("-h", "--help", "Show this message") do puts opts exit end + + opts.on("--version", "Show version") do + puts RemoteSyslog::VERSION + exit(0) + end + + opts.separator '' + opts.separator "Example:" + opts.separator " $ #{program_name} -c configs/logs.yml -p 12345 /var/log/mysqld.log" end op.parse!(@argv) @files = @argv.dup.delete_if { |a| a.empty? } - parse_config + if @configfile + if File.exists?(@configfile) + parse_config(@configfile) + else + error "The config file specified could not be found: #{@configfile}" + end + elsif File.exists?(DEFAULT_CONFIG_FILE) + parse_config(DEFAULT_CONFIG_FILE) + end - @dest_host ||= 'logs.papertrailapp.com' - @dest_port ||= 514 - if @files.empty? - puts "No filenames provided and #{@configfile} not found or malformed." - puts '' - puts op - exit + error "You must specify at least one file to watch" end + @agent.destination_host ||= 'logs.papertrailapp.com' + @agent.destination_port ||= 514 + # handle relative paths before Daemonize changes the wd to / and expand wildcards @files = @files.flatten.map { |f| File.expand_path(f) }.uniq + @agent.files = @files + + if @pid_directory + if @agent.pid_file + @agent.pid_file = File.expand_path("#{@pid_directory}/#{@agent.pid_file}") + else + @agent.pid_file = File.expand_path("#{@pid_directory}/remote_syslog.pid") + end + end + + @agent.pid_file ||= default_pid_file + + if !@no_detach && !::Servolux.fork? + @no_detach = true + + puts "Fork is not supported in this Ruby environment. Running in foreground." + end + rescue OptionParser::ParseError => e + error e.message, true end - def parse_config - if File.exist?(@configfile) - config = YAML.load_file(@configfile) + def parse_config(file) + config = YAML.load_file(file) - @files += Array(config['files']) + @files += Array(config['files']) - if config['destination'] && config['destination']['host'] - @dest_host ||= config['destination']['host'] - end + if config['destination'] && config['destination']['host'] + @agent.destination_host ||= config['destination']['host'] + end - if config['destination'] && config['destination']['port'] - @dest_port ||= config['destination']['port'] - end + if config['destination'] && config['destination']['port'] + @agent.destination_port ||= config['destination']['port'] + end - if config['hostname'] - @hostname = config['hostname'] - end + if config['hostname'] + @agent.hostname = config['hostname'] + end - @server_cert = config['ssl_server_cert'] - @client_cert_chain = config['ssl_client_cert_chain'] - @client_private_key = config['ssl_client_private_key'] + @agent.server_cert = config['ssl_server_cert'] + @agent.client_cert_chain = config['ssl_client_cert_chain'] + @agent.client_private_key = config['ssl_client_private_key'] - if config['parse_fields'] - @parse_fields = FIELD_REGEXES[config['parse_fields']] || Regexp.new(config['parse_fields']) - end + if config['parse_fields'] + @agent.parse_fields = FIELD_REGEXES[config['parse_fields']] || Regexp.new(config['parse_fields']) + end - if config['exclude_patterns'] - @exclude_pattern = Regexp.new(config['exclude_patterns'].map { |r| "(#{r})" }.join('|')) - end + if config['exclude_patterns'] + @agent.exclude_pattern = Regexp.new(config['exclude_patterns'].map { |r| "(#{r})" }.join('|')) end end def run - puts "Watching #{@files.length} files/paths. Sending to #{@dest_host}:#{@dest_port} (#{@tls ? 'TCP/TLS' : 'UDP'})." - if @no_detach - start + puts "Watching #{@agent.files.length} files/paths. Sending to #{@agent.destination_host}:#{@agent.destination_port} (#{@agent.tls ? 'TCP/TLS' : 'UDP'})." + @agent.run else - Daemons.run_proc(@app_name, @daemonize_options) do - start + daemon = Servolux::Daemon.new(:server => @agent) + + if daemon.alive? + error "Already running at #{@agent.pid_file}. To run another instance, specify a different `--pid-file`.", true end + + puts "Watching #{@agent.files.length} files/paths. Sending to #{@agent.destination_host}:#{@agent.destination_port} (#{@agent.tls ? 'TCP/TLS' : 'UDP'})." + daemon.startup end + rescue Servolux::Daemon::StartupError => e + case message = e.message[/^(Child raised error: )?(.*)$/, 2] + when /#<Errno::EACCES: (.*)>$/ + error $1 + else + error message + end + rescue Interrupt + exit(0) end - def start - EventMachine.run do - if @tls - connection = TlsEndpoint.new(@dest_host, @dest_port, - :client_cert_chain => @client_cert_chain, - :client_private_key => @client_private_key, - :server_cert => @server_cert) - else - connection = UdpEndpoint.new(@dest_host, @dest_port) - end - - @files.each do |path| - begin - glob_check_interval = 60 - exclude_files = [] - max_message_size = 1024 - - if @tls - max_message_size = 10240 - end - - EventMachine::FileGlobWatchTail.new(path, RemoteSyslog::Reader, - glob_check_interval, exclude_files, - @dest_host, @dest_port, - :socket => connection, :facility => @facility, - :severity => @severity, :strip_color => @strip_color, - :hostname => @hostname, :parse_fields => @parse_fields, - :exclude_pattern => @exclude_pattern, - :max_message_size => max_message_size) - rescue Errno::ENOENT => e - puts "#{path} not found, continuing. (#{e.message})" - end - end + def error(message, try_help = false) + puts "#{program_name}: #{message}" + if try_help + puts "Try `#{program_name} --help' for more information." end + exit(1) end end end