module Locd::Newsyslog

Use `newsyslog` to rotate {Locd::Agent} log files.

Public Class Methods

log_dir() click to toggle source
# File lib/locd/newsyslog.rb, line 327
def self.log_dir
  @log_dir ||= Locd.config.log_dir.join( Locd::Agent::RotateLogs.label )
end
log_to_file(&block) click to toggle source
# File lib/locd/newsyslog.rb, line 337
def self.log_to_file &block
  time = Time.now.iso8601
  date = time.split( 'T', 2 )[0]
  
  # Like
  #
  #   ~/.locd/log/com.nrser.locd.rotate-logs/2018-02-14/2018-02-14T03:46:57+08:00.log
  #
  path = self.log_dir / date / "#{ time }.log"
  
  FileUtils.mkdir_p( path.dirname ) unless path.dirname.exist?

  appender = SemanticLogger.add_appender \
    file_name: path.to_s
  
  begin
    result = block.call
  ensure
    SemanticLogger.remove_appender appender
  end
end
run(agent, tmp_conf_dir: self.tmp_conf_dir, keep_conf_files: false) click to toggle source

Run `newsyslog` for an agent to rotate it's log files (if present and needed).

@param [Locd::Agent] agent:

Agent to run for.

@param [Pathname | String] tmp_conf_dir:

Directory to write working files to, which are removed after successful
runs.

@return [Cmds::Result]

Result of running the `newsyslog` command.

@return [nil]

If we didn't run the command 'cause the agent doesn't have any logs
(that we know/care about).
# File lib/locd/newsyslog.rb, line 207
def self.run  agent,
              tmp_conf_dir: self.tmp_conf_dir,
              keep_conf_files: false
  logger.debug "Calling {.run_for} agent #{ agent.label }...",
    agent: agent,
    tmp_conf_dir: tmp_conf_dir
  
  # Make sure `tmp_conf_dir` is a {Pathname}
  tmp_conf_dir = tmp_conf_dir.to_pn
  
  # Collect the unique log paths
  log_paths = agent.log_paths
  
  if log_paths.empty?
    logger.info "Agent #{ agent.label } has no log files."
    return nil
  end
  
  logger.info "Setting up to run `newsyslog` for agent `#{ agent.label }`",
    log_paths: log_paths.map( &:to_s )
  
  # NOTE  Total race condition since agent may be started after this and
  #       before we rotate... f-it for now.
  pid_path = nil
  if pid = agent.pid( refresh: true )
    logger.debug "Agent is running", pid: pid
    
    pid_path = tmp_conf_dir / 'pids' / "#{ agent.label }.pid"
    FileUtils.mkdir_p( pid_path.dirname ) unless pid_path.dirname.exist?
    pid_path.write pid
    
    logger.debug "Wrote PID #{ pid } to file", pid_path: pid_path
  end
  
  entries = log_paths.map { |log_path|
    Entry.new log_path: log_path, pid_path: pid_path
  }
  conf_contents = entries.map( &:render ).join( "\n" ) + "\n"
  
  logger.debug "Generated conf entries",
    entries.map { |entry|
      [
        entry.log_path.to_s,
        entry.instance_variables.map { |name|
          entry.instance_variable_get name
        }
      ]
    }.to_h
  
  conf_path = tmp_conf_dir / 'confs' / "#{ agent.label }.conf"
  FileUtils.mkdir_p( conf_path.dirname ) unless conf_path.dirname.exist?
  conf_path.write conf_contents
  
  logger.debug "Wrote entries to conf file",
    conf_path: conf_path.to_s,
    conf_contents: conf_contents
  
  cmd = Cmds.new "newsyslog <%= opts %>", kwds: {
    opts: {
      # Turn on verbose output
      v: true,
      # Point to the conf file
      f: conf_path,
      # Don't run as root
      r: true,
    }
  }
  
  logger.info "Executing `#{ cmd.prepare }`"
  
  result = cmd.capture
  
  if result.ok?
    logger.info \
      "`newsyslog` command succeeded for agent `#{ agent.label }`" +
      ( result.out.empty? ?
        nil :
        ", output:\n" + result.out.indent(1, indent_string: '> ') )
    
    FileUtils.rm( pid_path ) if pid_path
    FileUtils.rm( conf_path ) unless keep_conf_files
    
    logger.debug "Files cleaned up."
    
  else
    logger.error "`newsyslog` command failed for agent #{ agent.label }",
      result: result.to_h
  end
  
  logger.debug "Returning",
    result: result.to_h
  
  result
end
run_all(tmp_conf_dir: self.tmp_conf_dir, trim_logs: true) click to toggle source

Call {.run} for each agent.

@param tmp_conf_dir: (see .run)

@return [Hash<Locd::Agent, Cmds::Result?>]

Hash mapping each agent to it's {.run} result (which may be `nil`).
# File lib/locd/newsyslog.rb, line 310
def self.run_all tmp_conf_dir: self.tmp_conf_dir, trim_logs: true
  log_to_file do
    Locd::Agent.all.values.
      reject { |agent|
        agent.label == Locd::Agent::RotateLogs.label
      }.
      map { |agent|
        [agent, run( agent, tmp_conf_dir: tmp_conf_dir )]
      }.
      to_h.
      tap { |_|
        self.trim_logs if trim_logs
      }
  end
end
tmp_conf_dir() click to toggle source
# File lib/locd/newsyslog.rb, line 332
def self.tmp_conf_dir
  @tmp_conf_dir ||= Locd.config.tmp_dir.join( Locd::Agent::RotateLogs.label )
end
trim_logs(keep_days: 7) click to toggle source
# File lib/locd/newsyslog.rb, line 360
def self.trim_logs keep_days: 7
  logger.info "Removing old self run log directories...",
    log_dir: self.log_dir.to_s,
    keep_days: keep_days
  
  unless self.log_dir.directory?
    logger.warn "{Locd::Newsyslog.log_dir} does not exist!",
      log_dir: self.log_dir
    return nil
  end
  
  day_dirs = self.log_dir.entries.select { |dir_name|
    dir_name.to_s =~ /\d{4}\-\d{2}\-\d{2}/ &&
      (self.log_dir / dir_name).directory?
  }
  
  to_remove = day_dirs.sort[0...(-1 * keep_days)]
  
  if to_remove.empty?
    logger.info "No old self run log directories to remove."
  else
    to_remove.each { |dir_name|
      path = self.log_dir / dir_name
      
      logger.info "Removing old day directory",
        path: path
      
      FileUtils.rm_rf path
    }
    
    logger.info "Done.",
      log_dir: self.log_dir.to_s,
      keep_days: keep_days
  end
  
  to_remove
end