Permalink
Browse files

Topic: merge Skeleton logging implemention.

I want to get this out before it gets any bigger so I can get feedback
on the overall design (i.e. how the classes fit together).

- Basic API for creating loggers
- Interfaces for core classes
-- Logger
-- LogRecord
-- Sink
-- Formatter

Change-Id: I18a2f7893edcb06639c69b7979c1afe6f7566010
  • Loading branch information...
1 parent f46c0ba commit 48d1718d8b02b5f2117b835439c4546294903463 mpage committed May 21, 2011
@@ -0,0 +1,7 @@
+source :rubygems
+
+gem 'rake'
+
+group :test do
+ gem 'rspec'
+end
@@ -0,0 +1,20 @@
+GEM
+ remote: http://rubygems.org/
+ specs:
+ diff-lcs (1.1.2)
+ rake (0.8.7)
+ rspec (2.5.0)
+ rspec-core (~> 2.5.0)
+ rspec-expectations (~> 2.5.0)
+ rspec-mocks (~> 2.5.0)
+ rspec-core (2.5.1)
+ rspec-expectations (2.5.0)
+ diff-lcs (~> 1.1.2)
+ rspec-mocks (2.5.0)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ rake
+ rspec
@@ -0,0 +1,35 @@
+# Overview
+
+This gem provides a common logging library to be used for all VCAP projects. It
+borrows heavily in its structure from the 'logging' gem.
+
+## Goals
+
+- Minimal dependencies. (Ideally, none.)
+- Compatible with both ruby18 and ruby19
+- Well tested.
+
+## Core Classes
+
+### Formatter
+
+Formatters take a log record and produce a string that can be written to a
+sink.
+
+### Log Sink
+
+Sinks are the final destination for log records. A sink must be configured with
+a formatter. Typically they wrap other objects that perform IO (such as files
+and sockets).
+
+### Logger
+
+Loggers are responsible for dispatching messages that need to be logged off to
+their appropriate sinks. They are arranged in a tree structure by name, such
+that log records flow from the leaves of the tree towards the root. The log
+record stops traversing the tree when it arrives at a logger that has been
+configured with a sink.
+
+### Trie
+
+Internally, a trie is used to manage the logger hierarchy.
View
@@ -0,0 +1,51 @@
+require 'rubygems'
+require 'rake'
+require 'rake/gempackagetask'
+
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
+require 'vcap/logging/version'
+
+GEM_NAME = 'vcap_logging'
+GEM_VERSION = VCAP::Logging::VERSION
+
+gemspec = Gem::Specification.new do |s|
+ s.name = GEM_NAME
+ s.version = GEM_VERSION
+ s.platform = Gem::Platform::RUBY
+ s.summary = 'Common logging library for VCAP projects'
+ s.description = s.summary
+ s.authors = ['Matt Page']
+ s.email = 'mpage@vmware.com'
+ s.homepage = 'http://www.cloudfoundry.com'
+ s.executables = [] # log_grep coming soon...
+ s.bindir = 'bin'
+ s.require_path = 'lib'
+ s.files = %w(README.md Rakefile) + Dir.glob("{lib,spec,vendor}/**/*")
+end
+
+Rake::GemPackageTask.new(gemspec) do |pkg|
+ pkg.gem_spec = gemspec
+end
+
+task :install => [:package] do
+ sh "gem install --no-ri --no-rdoc pkg/#{GEM_NAME}-#{GEM_VERSION}"
+end
+
+task :spec => ['bundler:install:test'] do
+ desc 'Run tests'
+ sh('cd spec && rake spec')
+end
+
+namespace 'bundler' do
+ task 'install' do
+ sh('bundle install')
+ end
+
+ environments = %w(test development production)
+ environments.each do |env|
+ desc "Install gems for #{env}"
+ task "install:#{env}" do
+ sh("bundle install --local --without #{(environments - [env]).join(' ')}")
+ end
+ end
+end
@@ -0,0 +1,4 @@
+- Sink impls.
+-- File
+-- Syslog
+- Formatter impl.
@@ -0,0 +1,100 @@
+require 'thread'
+
+module VCAP
+ module Logging
+
+ class LoggingError < StandardError; end
+
+ DEFAULT_DELIMITER = '.'
+
+ DEFAULT_LOG_LEVELS = {
+ :fatal => 0,
+ :error => 5,
+ :warn => 10,
+ :info => 15,
+ :debug => 16,
+ :debug1 => 17,
+ :debug2 => 18,
+ }
+
+ class << self
+
+ attr_accessor :default_log_level
+ attr_reader :log_level_map
+ attr_reader :sink_map
+
+ # Sets up the logging infrastructure
+ #
+ # @param opts Hash :log_levels => Hash log-levels along with their names
+ # :default_log_level => Symbol The log level to use if the logger has no parent in the hierarchy
+ # :delimiter => String Defines how names should be split in determining logger hierarchy
+ #
+ def init(opts={})
+ @log_level_map = opts[:log_levels] || DEFAULT_LOG_LEVELS
+ @delimiter = opts[:delimiter] || DEFAULT_DELIMITER
+ if opts[:default_log_level]
+ @default_log_level = opts[:default_log_level]
+ else
+ # The middle level seems like a reasonable default for the root logger
+ sorted_levels = @log_level_map.keys.sort {|a, b| @log_level_map[a] <=> @log_level_map[b] }
+ @default_log_level = sorted_levels[sorted_levels.length / 2]
+ end
+
+ VCAP::Logging::Logger.define_log_levels(@log_level_map)
+ @sink_map = VCAP::Logging::SinkMap.new(@log_level_map)
+ @loggers = {}
+ end
+
+ # Returns the logger associated with _name_. Creates one if it doesn't exist. The log level will be inherited
+ # from the parent logger.
+ #
+ # @param name String Logger name
+ def logger(name)
+ if !@loggers.has_key?(name)
+ @loggers[name] = VCAP::Logging::Logger.new(name, @sink_map)
+
+ # Not super efficient, but since we're not explicitly storing the parent-child relationships we
+ # must brute force it.
+ log_level = @default_log_level
+ off = name.rindex(@delimiter)
+ while off != nil
+ substr = name[0, off]
+ if @loggers[substr]
+ log_level = @loggers[substr].log_level
+ break
+ end
+ off = off > 2 ? name.rindex(@delimiter, off - 1) : nil
+ end
+ @loggers[name].log_level = log_level
+ end
+
+ @loggers[name]
+ end
+
+ # Sets the log level to _log_level_ for every logger whose name matches _path_regex_
+ #
+ # @param path_regex String Regular expression to use when matching against the logger name
+ # @param log_level_name Symbol Name of the log level to set on all matching loggers
+ def set_log_level(path_regex, log_level_name)
+ log_level_name = log_level_name.to_sym if log_level_name.kind_of?(String)
+
+ raise ArgumentError, "Unknown log level #{log_level_name}" unless @log_level_map[log_level_name]
+ regex = Regexp.new("^#{path_regex}$")
+
+ for logger_name, logger in @loggers
+ logger.log_level = log_level_name if regex.match(logger_name)
+ end
+ end
+
+ end
+ end
+end
+
+require 'vcap/logging/formatter'
+require 'vcap/logging/log_record'
+require 'vcap/logging/logger'
+require 'vcap/logging/sink'
+require 'vcap/logging/sink_map'
+require 'vcap/logging/version'
+
+VCAP::Logging.init
@@ -0,0 +1 @@
+require 'vcap/logging/formatter/base_formatter'
@@ -0,0 +1,30 @@
+require 'vcap/logging/log_record'
+
+module VCAP
+ module Logging
+ module Formatter
+
+ # Formatters are responsible for taking a log record and
+ # producing a string representation suitable for writing to a sink.
+ #
+ class BaseFormatter
+ # Produces a string suitable for writing to a sink
+ #
+ # @param log_record VCAP::Logging::LogRecord Log record to be formatted
+ # @return String
+ def format_record(log_record)
+ raise NotImplementedError
+ end
+
+ # The inverse of format_record()
+ #
+ # @param message String A string formatted using format_record()
+ # @return VCAP::Logging::LogRecord
+ def parse_message(message)
+ raise NotImplementedError
+ end
+ end
+
+ end
+ end
+end
@@ -0,0 +1,17 @@
+module VCAP
+ module Logging
+
+ class LogRecord
+ attr_reader :data
+ attr_reader :log_level
+ attr_reader :tags
+
+ def initialize(log_level, data, tags=[])
+ @data = data
+ @log_level = log_level
+ @tags = tags
+ end
+ end
+
+ end
+end
@@ -0,0 +1,102 @@
+require 'vcap/logging/log_record'
+
+module VCAP
+ module Logging
+ class Logger
+
+ # Loggers are responsible for dispatching log messages to an appropriate
+ # sink.
+
+ LogLevel = Struct.new(:name, :value)
+
+ class << self
+ attr_reader :log_levels
+
+ # Defines convenience methods for each log level. For example, if 'debug' is the name of a level
+ # corresponding 'debug' and 'debugf' instance methods will be defined for all loggers.
+ #
+ # @param levels Array[VCAP::Logging::LogLevel] Log levels to use
+ def define_log_levels(levels)
+
+ @prev_log_methods ||= []
+ # Clean up previously defined methods
+ for meth_name in @prev_log_methods
+ undef_method(meth_name)
+ end
+
+ @prev_log_methods = []
+ @log_levels = {}
+
+ # Partially evaluate log/logf for the level specified by each name
+ # 1.8.7 doesn't have optional block arguments, hence the use of class_eval() instead of define_method()
+ for name, level in levels
+ @log_levels[name] = LogLevel.new(name, level)
+
+ class_eval("def #{name}(data, opts={}); log(:#{name}, data, opts); end")
+ @prev_log_methods << name
+
+ name_f = name.to_s + 'f'
+ class_eval("def #{name_f}(fmt, fmt_args, opts={}); logf(:#{name}, fmt, fmt_args, opts); end")
+ @prev_log_methods << name_f.to_sym
+ end
+ end
+
+ end
+
+ attr_reader :name
+ attr_accessor :sink_map
+
+ def initialize(name, sink_map)
+ @name = name
+ @sink_map = sink_map
+ end
+
+ def log_level
+ @log_level.name
+ end
+
+ def log_level=(lvl_name)
+ level = self.class.log_levels[lvl_name]
+ raise ArgumentError, "Unknown level #{lvl_name}" unless level
+ @log_level = level
+
+ self
+ end
+
+ # Logs a message to the configured sinks. You may optionally supply a block to be called; its return value
+ # will be used as data for the log record.
+ #
+ # @param lvl_name Symbol Log level for the associated message
+ # @param data Object Optional data to log. How this is converted to a string is determined by the formatters.
+ # @param opts Hash :tags => Array[String] Tags to associated with this log message
+ def log(lvl_name, data=nil, opts={})
+ level = self.class.log_levels[lvl_name]
+ raise ArgumentError, "Unknown level #{lvl_name}" unless level
+
+ return unless level.value <= @log_level.value
+ data = yield if block_given?
+
+ rec = VCAP::Logging::LogRecord.new(lvl_name, data, opts[:tags] || [])
+ @sink_map.get_sinks(lvl_name).each {|s| s.add_record(rec) }
+ end
+
+ # Logs a message to the configured sinks. This is analogous to the printf() family
+ #
+ # @param lvl_name Symbol Log level for the associated message
+ # @param fmt String Format string to use when formatting the message
+ # @param fmt_args Array Arguments to format string
+ # @param opts Hash See log()
+ def logf(lvl_name, fmt, fmt_args, opts={})
+ level = self.class.log_levels[lvl_name]
+ raise ArgumentError, "Unknown level #{lvl_name}" unless level
+
+ return unless level.value <= @log_level.value
+ data = fmt % fmt_args
+
+ rec = VCAP::Logging::LogRecord.new(level, data, opts[:tags] || [])
+ @sink_map.get_sinks(lvl_name).each {|s| s.add_record(rec) }
+ end
+
+ end # VCAP::Logging::Logger
+ end
+end
@@ -0,0 +1 @@
+require 'vcap/logging/sink/base_sink'
Oops, something went wrong. Retry.

0 comments on commit 48d1718

Please sign in to comment.