Skip to content
Browse files

Refactored loops logging system. Added lots of comments for CLI inter…

…face
  • Loading branch information...
1 parent 9441836 commit 0dde6ff138cc1b49294182b11ff66a378693aada @kpumuk kpumuk committed Mar 15, 2010
View
3 .gitignore
@@ -1,3 +1,4 @@
.DS_Store
doc
-pkg
+pkg
+.yardoc
View
10 init.rb
@@ -1,9 +1 @@
-# Placeholder to satisfy Rails.
-#
-# Do NOT add any require statements to this file. Doing
-# so will cause Rails to load this plugin all of the time.
-#
-# Running 'ruby script/generate loops' will
-# generate script/loops file which includes the necessary
-# require statements and configuration. This file should
-# be used to run your loops.
+require File.join(File.dirname(__FILE__), 'lib/loops')
View
8 lib/loops.rb
@@ -3,7 +3,11 @@
require 'pathname'
module Loops
+ # @return [String]
+ # a full path to the loops "lib" directory.
LIB_ROOT = File.expand_path(File.dirname(__FILE__))
+ # @return [String]
+ # a full path to the loops binary file.
BINARY = File.expand_path(File.join(LIB_ROOT, '../bin/loops'))
def self.root
@@ -14,6 +18,10 @@ def self.root=(path)
@@root = Pathname.new(path)
end
+ def self.logger
+ @@logger ||= ::Loops::Logger.new($stdout)
+ end
+
def self.default_logger
@@default_logger
end
View
4 lib/loops/autoload.rb
@@ -8,9 +8,13 @@ def self.__p(*path) File.join(Loops::LIB_ROOT, 'loops', *path) end
autoload :Commands, __p('command')
autoload :Daemonize, __p('daemonize')
autoload :Engine, __p('engine')
+ autoload :Errors, __p('errors')
autoload :Logger, __p('logger')
autoload :ProcessManager, __p('process_manager')
autoload :Queue, __p('queue')
autoload :Worker, __p('worker')
autoload :WorkerPool, __p('worker_pool')
+ autoload :Version, __p('version')
+
+ include Errors
end
View
17 lib/loops/cli.rb
@@ -1,17 +1,32 @@
%w(commands options).each { |p| require File.join(Loops::LIB_ROOT, 'loops/cli', p) }
module Loops
+ # Command line interface for the Loops system.
+ #
+ # Used to parse command line options, initialize engine, and
+ # execute command requested.
+ #
+ # @example
+ # Loops::CLI.execute
+ #
class CLI
include Commands, Options
+ # Register all available commands.
register_command :list
register_command :debug
register_command :start
register_command :stop
- # The array of (unparsed) command-line options
+ # @return [Array<String>]
+ # The +Array+ of (unparsed) command-line options.
attr_reader :args
+ # Initializes a new instance of the {CLI} class.
+ #
+ # @param [Array<String>] args
+ # an +Array+ of command line arguments.
+ #
def initialize(args)
@args = args.dup
end
View
56 lib/loops/cli/commands.rb
@@ -1,37 +1,68 @@
module Loops
class CLI
+ # Contains methods related to Loops commands: retrieving, instantiating,
+ # executing.
+ #
+ # @example
+ # Loops::CLI.execute(ARGV)
+ #
module Commands
- def self.included(base) #:nodoc:
+ # @private
+ def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
+ # Parse arguments, find and execute command requested.
+ #
def execute
parse(ARGV).run!
end
+ # Register a Loops command.
+ #
+ # @param [Symbol, String] command_name
+ # a command name to register.
+ #
def register_command(command_name)
@@commands ||= {}
@@commands[command_name.to_sym] = nil
end
+ # Get a list of command names.
+ #
+ # @return [Array<String>]
+ # an +Array+ of command names.
+ #
def command_names
@@commands.keys.map { |c| c.to_s }
end
# Return the registered command from the command name.
+ #
+ # @param [Symbol, String] command_name
+ # a command name to register.
+ # @return [Command, nil]
+ # an instance of requested command.
+ #
def [](command_name)
command_name = command_name.to_sym
@@commands[command_name] ||= load_and_instantiate(command_name)
end
+ # Load and instantiate a given command.
+ #
+ # @param [Symbol, String] command_name
+ # a command name to register.
+ # @return [Command, nil]
+ # an instantiated command or +nil+, when command is not found.
+ #
def load_and_instantiate(command_name)
command_name = command_name.to_s
retried = false
begin
const_name = command_name.capitalize.gsub(/_(.)/) { $1.upcase }
- puts const_name.inspect
Loops::Commands.const_get("#{const_name}Command").new
rescue NameError
if retried then
@@ -45,6 +76,10 @@ def load_and_instantiate(command_name)
end
end
+ # Run command requested.
+ #
+ # Finds, instantiates and invokes a command.
+ #
def run!
if cmd = find_command(options[:command])
cmd.invoke(engine, options)
@@ -54,6 +89,13 @@ def run!
end
end
+ # Find and return an instance of {Command} by command name.
+ #
+ # @param [Symbol, String] command_name
+ # a command name to register.
+ # @return [Command, nil]
+ # an instantiated command or +nil+, when command is not found.
+ #
def find_command(command_name)
possibilities = find_command_possibilities(command_name)
if possibilities.size > 1 then
@@ -65,10 +107,18 @@ def find_command(command_name)
self.class[possibilities.first]
end
+ # Find command possibilities (used to find command by a short name).
+ #
+ # @param [Symbol, String] command_name
+ # a command name to register.
+ # @return [Array<String>]
+ # a list of possible commands matched to the specified short or
+ # full name.
+ #
def find_command_possibilities(command_name)
len = command_name.length
self.class.command_names.select { |c| command_name == c[0, len] }
end
end
end
-end
+end
View
93 lib/loops/cli/options.rb
@@ -3,26 +3,48 @@
module Loops
class CLI
+ # Contains methods to parse startup options, bootstrap application,
+ # and prepare #{CLI} class to run.
+ #
+ # @example
+ # Loops::CLI.parse(ARGV)
+ #
module Options
+ # @private
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
- # Return a new CLI instance with the given arguments pre-parsed and
+ # Return a new {CLI} instance with the given arguments pre-parsed and
# ready for execution.
+ #
+ # @param [Array<String>] args
+ # an +Array+ of options.
+ # @return [CLI]
+ # an instance of {CLI} with the given arguments pre-parsed.
+ #
def parse(args)
cli = new(args)
cli.parse_options!
cli
end
end
- # The hash of (parsed) command-line options
+ # @return [Hash<Symbol, Object>]
+ # The hash of (parsed) command-line options.
attr_reader :options
-
+
+ # @return [Engine]
+ # The loops engine instance.
attr_reader :engine
+ # Returns an option parser configured with all options
+ # available.
+ #
+ # @return [OptionParser]
+ # an option parser instance.
+ #
def option_parser
@option_parser ||= OptionParser.new do |opt|
opt.banner = "Usage: #{File.basename($0)} [options]"
@@ -58,6 +80,14 @@ def option_parser
end
end
+ # Parses startup options, bootstraps application, starts loops engine.
+ #
+ # Method exits process when unknown option passed or
+ # invalid value specified.
+ #
+ # @return [Hash]
+ # a hash of parsed options.
+ #
def parse_options!
@options = {
:daemonize => false,
@@ -80,33 +110,70 @@ def parse_options!
extract_command
bootstrap
start_engine
+
+ @options
end
- def extract_command
+ # Extracts command name from arguments.
+ #
+ # Other parameters are stored in the <tt>:args</tt> option
+ # of the {#options} hash.
+ #
+ # @return [String]
+ # a command name passed.
+ def extract_command!
options[:command], *options[:args] = args
+ options[:command]
end
+ # Detect the application root directory (contatining "app"
+ # subfolder).
+ #
+ # @return [String]
+ # absolute path of the application root directory.
+ #
def guess_root_dir
+ # Check for environment variable LOOP_ROOT containing
+ # the application root folder
+ if ENV['LOOPS_ROOT']
+ puts "Using root directory #{ENV['LOOPS_ROOT']} from LOOPS_ROOT environment variable"
+ return ENV['LOOPS_ROOT']
+ end
+
+ # Try to detect root dir (should contain app subfolder)
current_dir = Dir.pwd
loop do
if File.directory?(File.join(current_dir, 'app'))
- puts "Using root dir #{current_dir}"
+ # Found it!
+ puts "Using root directory #{current_dir}"
return current_dir
end
+ # Move up the FS hierarhy
pwd = File.expand_path(File.join(current_dir, '..'))
break if pwd == current_dir # if changing the directory made no difference, then we're at the top
current_dir = pwd
end
+ # Oops, not app folder found. Use the current dir as the root
current_dir = Dir.pwd
puts "Root directory guess failed. Using root dir #{current_dir}"
current_dir
end
+ # Application bootstrap.
+ #
+ # Checks framework option passed and load application
+ # stratup files conrresponding to its value. Also intitalizes
+ # the {Loops.default_logger} variable with the framework's
+ # default logger value.
+ #
+ # @raise [InvalidFrameworkError]
+ # occurred when unknown framework option value passed.
+ #
def bootstrap
case options[:framework]
- when 'rails' then
+ when 'rails'
ENV['RAILS_ENV'] = options[:environment] if options[:environment]
# Bootstrap Rails
@@ -115,7 +182,7 @@ def bootstrap
# Loops default logger
Loops.default_logger = Rails.logger
- when 'merb' then
+ when 'merb'
require 'merb-core'
ENV['MERB_ENV'] = options[:environment] if options[:environment]
@@ -129,20 +196,30 @@ def bootstrap
# Plain ruby loops
Loops.default_logger = Loops::Logger.new($stdout)
else
- abort "Invalid framework name: #{options[:framework]}. Valid values are: none, rails, merb."
+ raise InvalidFrameworkError, "Invalid framework name: #{options[:framework]}. Valid values are: none, rails, merb."
end
end
+ # Initializes a loops engine instance.
+ #
+ # Method loads and parses loops config file, and then
+ # initializes pid file path.
+ #
def start_engine
+ # Start loops engine
@engine = Loops::Engine.new
+ # If pid file option is not passed, get if from loops config ...
unless options[:pid_file] ||= @engine.global_config['pid_file']
+ # ... or try Rails' tmp/pids folder ...
options[:pid_file] = if Loops.root.join('tmp/pids').directory?
'tmp/pids/loops.pid'
else
+ # ... or use global system pids folder
'/var/run/loops.pid'
end
end
+ # Resolve relative pid file path
options[:pid_file] = Loops.root.join(options[:pid_file]).to_s unless options[:pid_file] =~ /^\//
end
end
View
22 lib/loops/command.rb
@@ -1,22 +1,36 @@
+# Represents a Loops command.
class Loops::Command
+ # @return [Engine]
+ # The instance of {Engine} to execute command in.
attr_reader :engine
-
+
+ # @return [Hash<String, Object>]
+ # The hash of (parsed) command-line options.
attr_reader :options
-
+
+ # Initializes a new {Command} instance.
def initialize
end
+ # Invoke a command.
+ #
+ # Initiaizes {#engine} and {#options} variables and
+ # executes a command.
+ #
def invoke(engine, options)
@engine = engine
@options = options
-
+
execute
end
-
+
+ # A command entry point. Should be overridden in descendants.
+ #
def execute
raise 'Generic command has no actions'
end
end
+# All Loops command registered.
module Loops::Commands
end
View
1 lib/loops/commands/debug_command.rb
@@ -1,5 +1,6 @@
class Loops::Commands::DebugCommand < Loops::Command
def execute
+ Loops.logger.write_to_console = true
puts "Starting one loop in debug mode: #{options[:args].first}"
engine.debug_loop!(options[:args].first)
exit(0)
View
17 lib/loops/engine.rb
@@ -5,8 +5,6 @@ class Loops::Engine
attr_reader :global_config
- attr_reader :logger
-
def initialize
load_config
end
@@ -20,12 +18,13 @@ def load_config
@global_config = @config['global']
@loops_config = @config['loops']
- @logger = ::Loops::Logger.new(@global_config['logger'] || $stdout)
+ Loops.logger.default_logfile = @global_config['logger'] || $stdout
+ Loops.logger.colorful_logs = @global_config['colorful_logs'] || @global_config['colourful_logs']
end
def start_loops!(loops_to_start = [])
@running_loops = []
- @pm = Loops::ProcessManager.new(global_config, logger)
+ @pm = Loops::ProcessManager.new(global_config, Loops.logger)
# Start all loops
loops_config.each do |name, config|
@@ -53,7 +52,7 @@ def start_loops!(loops_to_start = [])
end
def debug_loop!(loop_name)
- @pm = Loops::ProcessManager.new(global_config, logger)
+ @pm = Loops::ProcessManager.new(global_config, Loops.logger)
loop_config = loops_config[loop_name] || {}
# Adjust loop config values before starting it in debug mode
@@ -76,7 +75,7 @@ def debug_loop!(loop_name)
[ :debug, :error, :fatal, :info, :warn ].each do |meth_name|
class_eval <<-EVAL, __FILE__, __LINE__
def #{meth_name}(message)
- Loops.default_logger.#{meth_name} "loops[RUNNER/\#{Process.pid}]: \#{message}"
+ Loops.logger.#{meth_name} "loops[RUNNER/\#{Process.pid}]: \#{message}"
end
EVAL
end
@@ -120,10 +119,10 @@ def start_loop(name, klass, config)
loop_proc = Proc.new do
the_logger =
- if logger.is_a?(Loops::Logger) && @global_config['workers_engine'] == 'fork'
+ if Loops.logger.is_a?(Loops::Logger) && @global_config['workers_engine'] == 'fork'
# this is happening right after the fork, therefore no need for teardown at the end of the proc
- logger.logfile = config['logger']
- logger
+ Loops.logger.logfile = config['logger']
+ Loops.logger
else
# for backwards compatibility and handling threading engine
create_logger(name, config)
View
5 lib/loops/errors.rb
@@ -0,0 +1,5 @@
+module Loops::Errors
+ Error = Class.new(RuntimeError)
+
+ InvalidFrameworkError = Class.new(Error)
+end
View
102 lib/loops/logger.rb
@@ -2,7 +2,33 @@
require 'delegate'
class Loops::Logger < ::Delegator
-
+ # @return [Boolean]
+ # A value indicating whether all logging output should be
+ # also duplicated to the console.
+ attr_reader :write_to_console
+
+ # @return [Boolean]
+ # A value inidicating whether critical errors should be highlighted
+ # with ANSI colors in the log.
+ attr_reader :colorful_logs
+
+ # Initializes a new instance of the {Logger} class.
+ #
+ # @param [String, IO] logfile
+ # The log device. This is a filename (String), <tt>'stdout'</tt> or
+ # <tt>'stderr'</tt> (String), <tt>'default'</tt> for default framework's
+ # log file, or +IO+ object (typically +STDOUT+, +STDERR+,
+ # or an open file).
+ # @param [Integer] level
+ # Logging level. Constants are defined in +Logger+ namespace: +DEBUG+, +INFO+,
+ # +WARN+, +ERROR+, +FATAL+, or +UNKNOWN+.
+ # @param [Integer] number_of_files
+ # A number of files to keep.
+ # @param [Integer] max_file_size
+ # A max file size. When file become larger, next one will be created.
+ # @param [Boolean] write_to_console
+ # When +true+, all logging output will be dumped to the +STDOUT+ also.
+ #
def initialize(logfile = $stdout, level = ::Logger::INFO, number_of_files = 10, max_file_size = 100 * 1024 * 1024,
write_to_console = false)
@number_of_files, @level, @max_file_size, @write_to_console =
@@ -11,11 +37,28 @@ def initialize(logfile = $stdout, level = ::Logger::INFO, number_of_files = 10,
super(@implementation)
end
+ # Sets the default log file (see {#logfile=}).
+ #
+ # @param [String, IO] logfile
+ # the log file path or IO.
+ # @return [String, IO]
+ # the log file path or IO.
+ #
def default_logfile=(logfile)
@default_logfile = logfile
self.logfile = logfile
end
+ # Sets the log file.
+ #
+ # @param [String, IO] logfile
+ # The log device. This is a filename (String), <tt>'stdout'</tt> or
+ # <tt>'stderr'</tt> (String), <tt>'default'</tt> for default framework's
+ # log file, or +IO+ object (typically +STDOUT+, +STDERR+,
+ # or an open file).
+ # @return [String, IO]
+ # the log device.
+ #
def logfile=(logfile)
logfile = @default_logfile || $stdout if logfile == 'default'
coerced_logfile =
@@ -30,23 +73,62 @@ def logfile=(logfile)
logfile
end
end
- @implementation = LoggerImplementation.new(coerced_logfile, @number_of_files, @max_file_size, @write_to_console)
+ # Ensure logging directory does exist
+ FileUtils.mkdir_p(File.dirname(coerced_logfile))
+
+ # Create a logger implementation.
+ @implementation = LoggerImplementation.new(coerced_logfile, @number_of_files, @max_file_size, @write_to_console, @colorful_logs)
@implementation.level = @level
logfile
end
- # remember the level at the proxy level
+ # Remember the level at the proxy level.
+ #
+ # @param [Integer] level
+ # Logging severity.
+ # @return [Integer]
+ # Logging severity.
+ #
def level=(level)
@level = level
@implementation.level = @level if @implementation
level
end
- # send everything else to @implementation
+ # Sets a value indicating whether to dump all logs to the console.
+ #
+ # @param [Boolean] value
+ # a value indicating whether to dump all logs to the console.
+ # @return [Boolean]
+ # a value indicating whether to dump all logs to the console.
+ #
+ def write_to_console=(value)
+ @write_to_console = value
+ @implementation.write_to_console = value if @implementation
+ value
+ end
+
+ # Sets a value indicating whether to highlight with red ANSI color
+ # all critical messages.
+ #
+ # @param [Boolean] value
+ # a value indicating whether to highlight critical errors in log.
+ # @return [Boolean]
+ # a value indicating whether to highlight critical errors in log.
+ #
+ def colorful_logs=(value)
+ @colorful_logs = value
+ @implementation.colorful_logs = value if @implementation
+ value
+ end
+
+ # @private
+ # Send everything else to @implementation.
def __getobj__
@implementation or raise "Logger implementation not initialized"
end
+ # @private
# Delegator's method_missing ignores the &block argument (!!!?)
def method_missing(m, *args, &block)
target = self.__getobj__
@@ -57,10 +139,13 @@ def method_missing(m, *args, &block)
end
end
+ # @private
class LoggerImplementation < ::Logger
attr_reader :prefix
+ attr_accessor :write_to_console, :colorful_logs
+
class Formatter
def initialize(logger)
@@ -76,16 +161,17 @@ def call(severity, time, progname, message)
end
end
- def initialize(log_device, number_of_files = 10, max_file_size = 10 * 1024 * 1024, write_to_console = true)
+ def initialize(log_device, number_of_files = 10, max_file_size = 10 * 1024 * 1024, write_to_console = true, colorful_logs = false)
super(log_device, number_of_files, max_file_size)
- self.formatter = Formatter.new(self)
+ self.formatter = Formatter.new(self)
@write_to_console = write_to_console
- @prefix = nil
+ @colorful_logs = colorful_logs
+ @prefix = nil
end
def add(severity, message = nil, progname = nil, &block)
begin
- if Loops.config['colorful_logs'] || Loops.config['colourful_logs']
+ if @colorful_logs
message = color_errors(severity, message)
progname = color_errors(severity, progname)
end
View
31 lib/loops/version.rb
@@ -0,0 +1,31 @@
+# Contains information about currently used Loops version.
+#
+# @example
+# puts "Loops #{Loops::Version}"
+#
+class Loops::Version
+ # @return [Hash<Symbol, Integer>]
+ # a +Hash+ containing major, minor, and patch version parts.
+ CURRENT = YAML.load_file(File.join(Loops::LIB_ROOT, '../VERSION.yml'))
+
+ # @return [Integer]
+ # a major part of the Loops version.
+ MAJOR = CURRENT[:major]
+ # @return [Integer]
+ # a minor part of the Loops version.
+ MINOR = CURRENT[:minor]
+ # @return [Integer]
+ # a patch part of the Loops version.
+ PATCH = CURRENT[:patch]
+
+ # @return [String]
+ # a string representation of the Loops version.
+ STRING = "%d.%d.%d" % [MAJOR, MINOR, PATCH]
+
+ # @return [String]
+ # a string representation of the Loops version.
+ #
+ def self.to_s
+ STRING
+ end
+end

0 comments on commit 0dde6ff

Please sign in to comment.
Something went wrong with that request. Please try again.