├── lib ├── logging │ ├── filters.rb │ ├── version.rb │ ├── layouts.rb │ ├── filter.rb │ ├── rails_compat.rb │ ├── filters │ │ └── level.rb │ ├── layouts │ │ └── basic.rb │ ├── appenders.rb │ ├── root_logger.rb │ ├── log_event.rb │ ├── proxy.rb │ ├── appenders │ │ ├── string_io.rb │ │ ├── console.rb │ │ ├── io.rb │ │ ├── file.rb │ │ └── syslog.rb │ ├── utils.rb │ ├── repository.rb │ ├── layout.rb │ └── appender.rb ├── spec │ └── logging_helper.rb └── rspec │ └── logging_helper.rb ├── script ├── console └── bootstrap ├── .gitignore ├── examples ├── simple.rb ├── rails4.rb ├── loggers.rb ├── classes.rb ├── layouts.rb ├── rspec_integration.rb ├── fork.rb ├── appenders.rb ├── custom_log_levels.rb ├── formatting.rb ├── names.rb ├── reusing_layouts.rb ├── lazy.rb ├── colorization.rb ├── hierarchies.rb └── mdc.rb ├── test ├── appenders │ ├── test_string_io.rb │ ├── test_console.rb │ ├── test_io.rb │ ├── test_file.rb │ ├── test_syslog.rb │ ├── test_async_flushing.rb │ └── test_buffered_io.rb ├── setup.rb ├── test_filter.rb ├── test_color_scheme.rb ├── layouts │ ├── test_basic.rb │ ├── test_color_pattern.rb │ ├── test_nested_exceptions.rb │ ├── test_yaml.rb │ ├── test_json.rb │ └── test_pattern.rb ├── performance.rb ├── test_utils.rb ├── test_root_logger.rb ├── test_proxy.rb ├── test_log_event.rb ├── test_nested_diagnostic_context.rb ├── benchmark.rb ├── test_mapped_diagnostic_context.rb ├── test_repository.rb ├── test_layout.rb ├── test_appender.rb └── test_logging.rb ├── Rakefile ├── .github └── workflows │ └── ruby.yml ├── LICENSE ├── logging.gemspec └── README.md /lib/logging/filters.rb: -------------------------------------------------------------------------------- 1 | module Logging 2 | module Filters ; end 3 | require libpath('logging/filters/level') 4 | end 5 | -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "irb" 3 | require "rubygems" 4 | 5 | $LOAD_PATH.unshift "lib" 6 | require "logging" 7 | 8 | IRB.start 9 | -------------------------------------------------------------------------------- /lib/logging/version.rb: -------------------------------------------------------------------------------- 1 | module Logging 2 | VERSION = "2.4.0".freeze 3 | 4 | # Returns the version string for the library. 5 | def self.version 6 | VERSION 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/logging/layouts.rb: -------------------------------------------------------------------------------- 1 | 2 | module Logging 3 | module Layouts; end 4 | 5 | require libpath('logging/layouts/basic') 6 | require libpath('logging/layouts/parseable') 7 | require libpath('logging/layouts/pattern') 8 | end # Logging 9 | 10 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | gem_install () { 4 | gem list -i $1 >/dev/null 2>&1 5 | if [ $? -ne 0 ]; then 6 | gem install $1 7 | fi 8 | } 9 | 10 | gem_install 'bones' 11 | gem_install 'rdoc' 12 | 13 | rake gem:install_dependencies 14 | -------------------------------------------------------------------------------- /lib/spec/logging_helper.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('../../rspec/logging_helper', __FILE__) 3 | Spec::LoggingHelper = RSpec::LoggingHelper 4 | 5 | if defined? Spec::Runner::Configuration 6 | class Spec::Runner::Configuration 7 | include Spec::LoggingHelper 8 | end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # git-ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | announcement.txt 8 | coverage/ 9 | doc/ 10 | pkg/ 11 | tmp/ 12 | vendor/ 13 | .rbx 14 | .rvmrc 15 | .tool-versions 16 | -------------------------------------------------------------------------------- /examples/simple.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # Logging provides a simple, default logger configured in the same manner as 4 | # the default Ruby Logger class -- i.e. the output of the two will be the 5 | # same. All log messages at "warn" or higher are printed to STDOUT; any 6 | # message below the "warn" level are discarded. 7 | # 8 | 9 | require 'logging' 10 | 11 | log = Logging.logger(STDOUT) 12 | log.level = :warn 13 | 14 | log.debug "this debug message will not be output by the logger" 15 | log.warn "this is your last warning" 16 | 17 | # :startdoc: 18 | -------------------------------------------------------------------------------- /examples/rails4.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # Rails 4 allows you to hook up multiple loggers (even those external to this gem) 4 | # so you can use a single Rails.logger statement. For Rails developers, this is 5 | # easier because if you ever change logging frameworks, you don't have to change 6 | # all of your app code. 7 | # 8 | # See http://railsware.com/blog/2014/08/07/rails-logging-into-several-backends/ 9 | # 10 | 11 | require 'logging' 12 | 13 | log = Logging.logger(STDOUT) 14 | log.level = :warn 15 | 16 | Rails.logger.extend(ActiveSupport::Logger.broadcast(log)) 17 | 18 | Rails.logger.debug "this debug message will not be output by the logger" 19 | Rails.logger.warn "this is your last warning" 20 | 21 | # :startdoc: 22 | -------------------------------------------------------------------------------- /test/appenders/test_string_io.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('../setup', File.dirname(__FILE__)) 3 | 4 | module TestLogging 5 | module TestAppenders 6 | 7 | class TestStringIO < Test::Unit::TestCase 8 | include LoggingTestCase 9 | 10 | def setup 11 | super 12 | 13 | @appender = Logging.appenders.string_io('test_appender') 14 | @sio = @appender.sio 15 | @levels = Logging::LEVELS 16 | end 17 | 18 | def teardown 19 | @appender.close 20 | @appender = nil 21 | super 22 | end 23 | 24 | def test_reopen 25 | assert_equal @sio.object_id, @appender.sio.object_id 26 | 27 | @appender.reopen 28 | assert @sio.closed?, 'StringIO instance is closed' 29 | assert_not_equal @sio.object_id, @appender.sio.object_id 30 | end 31 | 32 | end # class TestStringIO 33 | 34 | end # module TestAppenders 35 | end # module TestLogging 36 | 37 | -------------------------------------------------------------------------------- /test/setup.rb: -------------------------------------------------------------------------------- 1 | # Equivalent to a header guard in C/C++ 2 | # Used to prevent the class/module from being loaded more than once 3 | unless defined? LOGGING_TEST_SETUP 4 | LOGGING_TEST_SETUP = true 5 | 6 | require "rubygems" 7 | require "test/unit" 8 | require "tmpdir" 9 | 10 | LOGGING_TEST_TMPDIR = Dir.mktmpdir("logging") 11 | Test::Unit.at_exit do 12 | FileUtils.remove_entry(LOGGING_TEST_TMPDIR) 13 | end 14 | 15 | if Test::Unit::TestCase.respond_to? :test_order= 16 | Test::Unit::TestCase.test_order = :random 17 | end 18 | 19 | require File.expand_path("../../lib/logging", __FILE__) 20 | 21 | module TestLogging 22 | module LoggingTestCase 23 | 24 | def setup 25 | super 26 | Logging.reset 27 | @tmpdir = LOGGING_TEST_TMPDIR 28 | FileUtils.rm_rf(Dir.glob(File.join(@tmpdir, "*"))) 29 | end 30 | 31 | def teardown 32 | super 33 | FileUtils.rm_rf(Dir.glob(File.join(@tmpdir, "*"))) 34 | end 35 | end 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /lib/logging/filter.rb: -------------------------------------------------------------------------------- 1 | module Logging 2 | 3 | # The `Filter` class allows for filtering messages based on event 4 | # properties independently of the standard minimum-level restriction. 5 | # 6 | # All other Filters inherit from this class, and must override the 7 | # `allow` method to return the event if it should be allowed into the log. 8 | # Otherwise the `allow` method should return `nil`. 9 | class Filter 10 | 11 | # Creates a new level filter that will pass all log events. Create a 12 | # subclass and override the `allow` method to filter log events. 13 | def initialize 14 | ::Logging.init unless ::Logging.initialized? 15 | end 16 | 17 | # Returns the event if it should be forwarded to the logging appender. 18 | # Returns `nil` if the event should _not_ be forwarded to the logging 19 | # appender. Subclasses should override this method and provide their own 20 | # filtering semantics. 21 | def allow(event) 22 | event 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bones' 3 | rescue LoadError 4 | abort '### please install the "bones" gem ###' 5 | end 6 | 7 | lib = File.expand_path('../lib', __FILE__) 8 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 9 | require 'logging/version' 10 | 11 | task :default => 'test:run' 12 | task 'gem:release' => 'test:run' 13 | 14 | Bones { 15 | name 'logging' 16 | summary 'A flexible and extendable logging library for Ruby' 17 | authors 'Tim Pease' 18 | email 'tim.pease@gmail.com' 19 | url 'http://rubygems.org/gems/logging' 20 | version Logging::VERSION 21 | license 'MIT' 22 | 23 | rdoc.exclude << '^data' 24 | rdoc.include << '^examples/.*\.rb' 25 | #rcov.opts << '-x' << '~/.rvm/' 26 | 27 | use_gmail 28 | 29 | depend_on 'little-plugger', '~> 1.1' 30 | depend_on 'multi_json', '~> 1.14' 31 | 32 | depend_on 'test-unit', '~> 3.3', development: true 33 | depend_on 'bones-git', '~> 1.3', development: true 34 | #depend_on 'bones-rcov', development: true 35 | } 36 | 37 | -------------------------------------------------------------------------------- /examples/loggers.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # Multiple loggers can be created and each can be configured with it's own 4 | # log level and appenders. So one logger can be configured to output debug 5 | # messages, and all the others can be left at the info or warn level. This 6 | # makes it easier to debug specific portions of your code. 7 | # 8 | 9 | require 'logging' 10 | 11 | # all loggers inherit the log level of the "root" logger 12 | # but specific loggers can be given their own level 13 | Logging.logger.root.level = :warn 14 | 15 | # similarly, the root appender will be used by all loggers 16 | Logging.logger.root.appenders = Logging.appenders.file('output.log') 17 | 18 | log1 = Logging.logger['Log1'] 19 | log2 = Logging.logger['Log2'] 20 | log3 = Logging.logger['Log3'] 21 | 22 | # you can use strings or symbols to set the log level 23 | log3.level = 'debug' 24 | 25 | log1.info "this message will not get logged" 26 | log2.info "nor will this message" 27 | log3.info "but this message will get logged" 28 | 29 | # :startdoc: 30 | -------------------------------------------------------------------------------- /test/test_filter.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('setup', File.dirname(__FILE__)) 2 | 3 | module TestLogging 4 | 5 | class TestFilter < Test::Unit::TestCase 6 | include LoggingTestCase 7 | 8 | def setup 9 | super 10 | 11 | ::Logging::init 12 | @lf = ::Logging::Filters::Level.new :debug, :warn 13 | end 14 | 15 | def test_level_filter_includes_selected_level 16 | debug_evt = event_for_level(:debug) 17 | warn_evt = event_for_level(:warn) 18 | assert_same debug_evt, @lf.allow(debug_evt), "Debug messages should be allowed" 19 | assert_same warn_evt, @lf.allow(warn_evt), "Warn messages should be allowed" 20 | end 21 | 22 | def test_level_filter_excludes_unselected_level 23 | event = event_for_level(:info) 24 | assert_nil @lf.allow(event), "Info messages should be disallowed" 25 | end 26 | 27 | def event_for_level(level) 28 | ::Logging::LogEvent.new('logger', ::Logging::LEVELS[level.to_s], 29 | 'message', false) 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/logging/rails_compat.rb: -------------------------------------------------------------------------------- 1 | module Logging 2 | 3 | # Rails compatibility module. 4 | # 5 | # The ActiveSupport gem adds a few methods to the default Ruby logger, and 6 | # some Rails extensions expect these methods to exist. Those methods are 7 | # implemented in this module and included in the Logging::Logger class when 8 | # the ActiveSupport gem is present. 9 | module RailsCompat 10 | 11 | # A no-op implementation of the `formatter` method. 12 | def formatter; end 13 | 14 | # A no-op implementation of the `formatter=` method. 15 | def formatter=(_formatter); end 16 | 17 | # A no-op implementation of the +silence+ method. Setting of log levels 18 | # should be done during the Logging configuration. It is the author's 19 | # opinion that overriding the log level programmatically is a logical 20 | # error. 21 | # 22 | # Please see https://github.com/TwP/logging/issues/11 for a more detailed 23 | # discussion of the issue. 24 | def silence( *args ) 25 | yield self 26 | end 27 | end 28 | 29 | Logger.send :include, RailsCompat 30 | end 31 | -------------------------------------------------------------------------------- /lib/rspec/logging_helper.rb: -------------------------------------------------------------------------------- 1 | 2 | module RSpec 3 | module LoggingHelper 4 | 5 | # Capture log messages from the Logging framework and make them 6 | # available via a @log_output instance variable. The @log_output 7 | # supports a readline method to access the log messages. 8 | # 9 | def capture_log_messages( opts = {} ) 10 | from = opts.fetch(:from, 'root') 11 | to = opts.fetch(:to, '__rspec__') 12 | exclusive = opts.fetch(:exclusive, true) 13 | 14 | appender = Logging::Appenders[to] || Logging::Appenders::StringIo.new(to) 15 | logger = Logging::Logger[from] 16 | if exclusive 17 | logger.appenders = appender 18 | else 19 | logger.add_appenders(appender) 20 | end 21 | 22 | before(:all) do 23 | @log_output = Logging::Appenders[to] 24 | end 25 | 26 | before(:each) do 27 | @log_output.reset 28 | end 29 | end 30 | 31 | end # module LoggingHelper 32 | end # module RSpec 33 | 34 | if defined? RSpec::Core::Configuration 35 | class RSpec::Core::Configuration 36 | include RSpec::LoggingHelper 37 | end 38 | end 39 | 40 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | ruby-version: ['2.7', '3.1', '3.2', '3.3', 'jruby'] 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | 26 | - name: Setup Ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: ${{ matrix.ruby-version }} 30 | bundler-cache: true 31 | 32 | - name: Setup Gem Cache 33 | uses: actions/cache@v4 34 | with: 35 | path: vendor/bundle 36 | key: ${{ runner.os }}-gems-${{ matrix.ruby-version }}-${{ hashFiles('**/Gemfile.lock') }} 37 | restore-keys: | 38 | ${{ runner.os }}-gems-${{ matrix.ruby-version }}- 39 | 40 | - name: Install Dependencies 41 | run: | 42 | gem install bones 43 | rake gem:install_dependencies 44 | 45 | - name: Run Tests 46 | run: rake test 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2007-2022 Tim Pease 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/logging/filters/level.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module Logging 4 | module Filters 5 | 6 | # The `Level` filter class provides a simple level-based filtering mechanism 7 | # that allows events whose log level matches a preconfigured list of values. 8 | class Level < ::Logging::Filter 9 | 10 | # Creates a new level filter that will only allow the given _levels_ to 11 | # propagate through to the logging destination. The _levels_ should be 12 | # given in symbolic form. 13 | # 14 | # Examples 15 | # Logging::Filters::Level.new(:debug, :info) 16 | # 17 | def initialize(*levels) 18 | super() 19 | levels = levels.flatten.map {|level| ::Logging::level_num(level)} 20 | @levels = Set.new(levels) 21 | end 22 | 23 | # Returns the event if it should be forwarded to the logging appender. 24 | # Otherwise, `nil` is returned. The log event is allowed if the 25 | # `event.level` matches one of the levels provided to the filter when it 26 | # was constructred. 27 | def allow(event) 28 | @levels.include?(event.level) ? event : nil 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/test_color_scheme.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('../setup', __FILE__) 3 | 4 | module TestLogging 5 | 6 | class TestColorScheme < Test::Unit::TestCase 7 | include LoggingTestCase 8 | 9 | def setup 10 | super 11 | ::Logging.init 12 | end 13 | 14 | def test_default_color_scheme 15 | scheme = Logging.color_scheme :default 16 | assert_instance_of ::Logging::ColorScheme, scheme 17 | 18 | assert_equal false, scheme.include?(:debug) 19 | assert scheme.include?(:info) 20 | assert scheme.include?(:warn) 21 | assert scheme.include?(:error) 22 | assert scheme.include?(:fatal) 23 | end 24 | 25 | def test_lines_levels_exclusivity 26 | assert_raise(ArgumentError) { Logging.color_scheme(:error, :lines => {}, :levels => {}) } 27 | end 28 | 29 | def test_colorization 30 | scheme = Logging.color_scheme :default 31 | 32 | assert_equal "no change", scheme.color('no change', :debug) 33 | assert_equal "\e[32minfo is green\e[0m", scheme.color('info is green', :info) 34 | assert_equal "\e[37m\e[41mfatal has multiple color codes\e[0m", scheme.color('fatal has multiple color codes', :fatal) 35 | end 36 | 37 | end # TestColorScheme 38 | end # TestLogging 39 | 40 | -------------------------------------------------------------------------------- /examples/classes.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # The Logging framework is very good about figuring out predictable names 4 | # for loggers regardless of what object is used to create them. The name is 5 | # the class name or module name of whatever is passed to the logger bracket 6 | # method. The following lines all return the exact same logger instance: 7 | # 8 | # ary = Array.new 9 | # Logging.logger[ary] 10 | # Logging.logger[Array] 11 | # Logging.logger['Array'] 12 | # Logging.logger[:Array] 13 | # 14 | # So, if you want each class to have it's own logger this is very easy to 15 | # do. 16 | # 17 | 18 | require 'logging' 19 | 20 | Logging.logger.root.appenders = Logging.appenders.stdout 21 | Logging.logger.root.level = :info 22 | 23 | class Foo 24 | attr_reader :log 25 | def initialize() @log = Logging.logger[self]; end 26 | end 27 | 28 | class Foo::Bar 29 | attr_reader :log 30 | def initialize() @log = Logging.logger[self]; end 31 | end 32 | 33 | foo = Foo.new.log 34 | bar = Foo::Bar.new.log 35 | 36 | # you'll notice in these log messages that the logger names were taken 37 | # from the class names of the Foo and Foo::Bar instances 38 | foo.info 'this message came from Foo' 39 | bar.warn 'this is a warning from Foo::Bar' 40 | 41 | # :startdoc: 42 | -------------------------------------------------------------------------------- /test/layouts/test_basic.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('../setup', File.dirname(__FILE__)) 3 | 4 | module TestLogging 5 | module TestLayouts 6 | 7 | class TestBasic < Test::Unit::TestCase 8 | include LoggingTestCase 9 | 10 | def setup 11 | super 12 | @layout = Logging.layouts.basic({}) 13 | @levels = Logging::LEVELS 14 | end 15 | 16 | def test_format 17 | event = Logging::LogEvent.new( 'ArrayLogger', @levels['info'], 18 | 'log message', false) 19 | assert_equal " INFO ArrayLogger : log message\n", @layout.format(event) 20 | 21 | event.data = [1, 2, 3, 4] 22 | assert_equal(" INFO ArrayLogger : #{[1,2,3,4]}\n", 23 | @layout.format(event)) 24 | 25 | event.level = @levels['debug'] 26 | event.data = 'and another message' 27 | log = "DEBUG ArrayLogger : and another message\n" 28 | assert_equal log, @layout.format(event) 29 | 30 | event.logger = 'Test' 31 | event.level = @levels['fatal'] 32 | event.data = Exception.new 33 | log = "FATAL Test : Exception\n" 34 | assert_equal log, @layout.format(event) 35 | end 36 | 37 | end # class TestBasic 38 | 39 | end # module TestLayouts 40 | end # module TestLogging 41 | 42 | -------------------------------------------------------------------------------- /lib/logging/layouts/basic.rb: -------------------------------------------------------------------------------- 1 | 2 | module Logging::Layouts 3 | 4 | # Accessor / Factory for the Basic layout. 5 | # 6 | def self.basic( *args ) 7 | return ::Logging::Layouts::Basic if args.empty? 8 | ::Logging::Layouts::Basic.new(*args) 9 | end 10 | 11 | # The +Basic+ layout class provides methods for simple formatting of log 12 | # events. The resulting string follows the format below. 13 | # 14 | # LEVEL LoggerName : log message 15 | # 16 | # _LEVEL_ is the log level of the event. _LoggerName_ is the name of the 17 | # logger that generated the event. log message is the message 18 | # or object that was passed to the logger. If multiple message or objects 19 | # were passed to the logger then each will be printed on its own line with 20 | # the format show above. 21 | # 22 | class Basic < ::Logging::Layout 23 | 24 | # call-seq: 25 | # format( event ) 26 | # 27 | # Returns a string representation of the given logging _event_. See the 28 | # class documentation for details about the formatting used. 29 | # 30 | def format( event ) 31 | obj = format_obj(event.data) 32 | sprintf("%*s %s : %s\n", ::Logging::MAX_LEVEL_LENGTH, 33 | ::Logging::LNAMES[event.level], event.logger, obj) 34 | end 35 | 36 | end # Basic 37 | end # Logging::Layouts 38 | 39 | -------------------------------------------------------------------------------- /examples/layouts.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # The formatting of log messages is controlled by the layout given to the 4 | # appender. By default all appenders use the Basic layout. It's pretty 5 | # basic. However, a more sophisticated Pattern layout can be used or one of 6 | # the Parseable layouts -- JSON or YAML. 7 | # 8 | # The available layouts are: 9 | # 10 | # Logging.layouts.basic 11 | # Logging.layouts.pattern 12 | # Logging.layouts.json 13 | # Logging.layouts.yaml 14 | # 15 | # In this example we'll demonstrate use of different layouts and setting log 16 | # levels in the appenders to filter out events. 17 | # 18 | 19 | require 'logging' 20 | 21 | # only show "info" or higher messages on STDOUT using the Basic layout 22 | Logging.appenders.stdout(:level => :info) 23 | 24 | # send all log events to the development log (including debug) as JSON 25 | Logging.appenders.rolling_file( 26 | 'development.log', 27 | :age => 'daily', 28 | :layout => Logging.layouts.json 29 | ) 30 | 31 | log = Logging.logger['Foo::Bar'] 32 | log.add_appenders 'stdout', 'development.log' 33 | log.level = :debug 34 | 35 | log.debug "a very nice little debug message" 36 | log.info "things are operating normally" 37 | log.warn "this is your last warning" 38 | log.error StandardError.new("something went horribly wrong") 39 | log.fatal "I Die!" 40 | 41 | # :startdoc: 42 | -------------------------------------------------------------------------------- /examples/rspec_integration.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # One useful feature of log messages in your code is that they provide a 4 | # convenient instrumentation point for testing. Through log messages you can 5 | # confirm that internal methods were called or that certain code paths were 6 | # executed. This example demonstrates how to capture log output during testing 7 | # for later analysis. 8 | # 9 | # The Logging framework provides an RSpec helper that will direct log output 10 | # to a StringIO appender. Log lines can be read from this IO destination 11 | # during tests. 12 | # 13 | 14 | require 'rspec' 15 | require 'logging' 16 | require 'rspec/logging_helper' 17 | 18 | # Configure RSpec to capture log messages for each test. The output from the 19 | # logs will be stored in the @log_output variable. It is a StringIO instance. 20 | RSpec.configure do |config| 21 | include RSpec::LoggingHelper 22 | config.capture_log_messages 23 | end 24 | 25 | # Now within your specs you can check that various log events were generated. 26 | describe 'SuperLogger' do 27 | it 'should be able to read a log message' do 28 | logger = Logging.logger['SuperLogger'] 29 | 30 | logger.debug 'foo bar' 31 | logger.warn 'just a little warning' 32 | 33 | @log_output.readline.should be == 'DEBUG SuperLogger: foo bar' 34 | @log_output.readline.should be == 'WARN SuperLogger: just a little warning' 35 | end 36 | end 37 | 38 | # :startdoc: 39 | -------------------------------------------------------------------------------- /examples/fork.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # Because of the global interpreter lock, Kernel#fork is the best way 4 | # to achieve true concurrency in Ruby scripts. However, there are peculiarities 5 | # when using fork and passing file descriptors between process. These 6 | # peculiarities affect the logging framework. 7 | # 8 | # In short, always reopen file descriptors in the child process after fork has 9 | # been called. The RollingFile appender uses flock to safely coordinate the 10 | # rolling of the log file when multiple processes are writing to the same 11 | # file. If the file descriptor is opened in the parent and multiple children 12 | # are forked, then each child will use the same file descriptor lock; when one 13 | # child locks the file any other child will also have the lock. This creates a 14 | # race condition in the rolling code. The solution is to reopen the file to 15 | # obtain a new file descriptor in each of the children. 16 | # 17 | 18 | require 'logging' 19 | 20 | log = Logging.logger['example'] 21 | log.add_appenders( 22 | Logging.appenders.rolling_file('roller.log', :age => 'daily') 23 | ) 24 | log.level = :debug 25 | 26 | # Create four child processes and reopen the "roller.log" file descriptor in 27 | # each child. Now log rolling will work safely. 28 | 4.times do 29 | fork { 30 | Logging.reopen 31 | log.info "This is child process #{Process.pid}" 32 | } 33 | end 34 | 35 | log.info "This is the parent process #{Process.pid}" 36 | 37 | # :startdoc: 38 | -------------------------------------------------------------------------------- /examples/appenders.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # Appenders are used to output log events to some logging destination. The 4 | # same log event can be sent to multiple destinations by associating 5 | # multiple appenders with the logger. 6 | # 7 | # The following is a list of all the available appenders and a brief 8 | # description of each. Please refer to the documentation for specific 9 | # configuration options available for each. 10 | # 11 | # File writes to a regular file 12 | # IO generic IO appender 13 | # RollingFile writes to a file and rolls based on size or age 14 | # Stdout appends to STDOUT 15 | # Stderr appends to STDERR 16 | # StringIo writes to a StringIO instance (useful for testing) 17 | # Syslog outputs to syslogd (not available on all systems) 18 | # 19 | # And you can access these appenders: 20 | # 21 | # Logging.appenders.file 22 | # Logging.appenders.io 23 | # Logging.appenders.rolling_file 24 | # Logging.appenders.stdout 25 | # Logging.appenders.stderr 26 | # Logging.appenders.string_io 27 | # Logging.appenders.syslog 28 | # 29 | 30 | require 'logging' 31 | 32 | log = Logging.logger['example'] 33 | log.add_appenders( 34 | Logging.appenders.stdout, 35 | Logging.appenders.file('development.log') 36 | ) 37 | log.level = :debug 38 | 39 | # These messages will be logged to both the log file and to STDOUT 40 | log.debug "a very nice little debug message" 41 | log.warn "this is your last warning" 42 | 43 | # :startdoc: 44 | -------------------------------------------------------------------------------- /examples/custom_log_levels.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # It's useful to define custom log levels that denote success, or otherwise 4 | # meaningful events that happen to not be negative (more than 50% of the 5 | # levels are given to warn, error, fail - quite a pessimistic view of one's 6 | # application's chances of success, no? ;-) ) 7 | # 8 | # Here, we define two new levels, 'happy' and 'success' and make them soothing 9 | # colours. 10 | # 11 | 12 | require 'logging' 13 | 14 | # https://github.com/TwP/logging/blob/master/lib/logging.rb#L250-285 15 | # The levels run from lowest level to highest level. 16 | 17 | Logging.init :debug, :info, :happy, :warn, :success, :error, :fatal 18 | 19 | Logging.color_scheme( 'soothing_ish', 20 | :levels => { 21 | :info => :cyan, 22 | :happy => :green, 23 | :warn => :yellow, 24 | :success => [:blue], 25 | :error => :red, 26 | :fatal => [:white, :on_red] 27 | }, 28 | :date => :cyan, 29 | :logger => :cyan, 30 | :message => :orange 31 | ) 32 | 33 | Logging.appenders.stdout( 34 | 'stdout', 35 | :layout => Logging.layouts.pattern( 36 | :pattern => '[%d] %-7l %c: %m\n', 37 | :color_scheme => 'soothing_ish' 38 | ) 39 | ) 40 | 41 | log = Logging.logger['Soothing::Colors'] 42 | log.add_appenders 'stdout' 43 | log.level = :debug 44 | 45 | log.debug 'a very nice little debug message' 46 | log.info 'things are operating nominally' 47 | log.happy 'What a beautiful day' 48 | log.warn 'this is your last warning' 49 | log.success 'I am INWEENCIBLE!!' 50 | log.error StandardError.new('something went horribly wrong') 51 | log.fatal 'I Die!' 52 | 53 | # :startdoc: 54 | -------------------------------------------------------------------------------- /examples/formatting.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # Any Ruby object can be passed to the log methods of a logger. How these 4 | # objects are formatted by the Logging framework is controlled by a global 5 | # "format_as" option and a global "backtrace" option. 6 | # 7 | # The format_as option allows objects to be converted to a string using the 8 | # standard "to_s" method, the "inspect" method, the "to_json" method, or the 9 | # "to_yaml" method (this is independent of the YAML layout). The format_as 10 | # option can be overridden by each layout as desired. 11 | # 12 | # Logging.format_as :string # or :inspect or :json or :yaml 13 | # 14 | # Exceptions are treated differently by the logging framework. The Exception 15 | # class is printed along with the message. Optionally, the exception backtrace 16 | # can be included in the logging output; this option is enabled by default. 17 | # 18 | # Logging.backtrace false 19 | # 20 | # The backtrace can be enabled or disabled for each layout as needed. 21 | # 22 | 23 | require 'logging' 24 | 25 | Logging.format_as :inspect 26 | Logging.backtrace false 27 | 28 | Logging.appenders.stdout( 29 | :layout => Logging.layouts.basic(:format_as => :yaml) 30 | ) 31 | 32 | Logging.appenders.stderr( 33 | :layout => Logging.layouts.basic(:backtrace => true) 34 | ) 35 | 36 | log = Logging.logger['foo'] 37 | log.appenders = %w[stdout stderr] 38 | 39 | # these log messages will all appear twice because of the two appenders - 40 | # STDOUT and STDERR - but the interesting thing is the difference in the 41 | # output 42 | log.info %w[An Array Of Strings] 43 | log.info({"one"=>1, "two"=>2}) 44 | 45 | begin 46 | 1 / 0 47 | rescue => err 48 | log.error err 49 | end 50 | 51 | # :startdoc: 52 | -------------------------------------------------------------------------------- /lib/logging/appenders.rb: -------------------------------------------------------------------------------- 1 | 2 | module Logging 3 | module Appenders 4 | 5 | # call-seq: 6 | # Appenders[name] 7 | # 8 | # Returns the appender instance stored in the appender hash under the 9 | # key _name_, or +nil+ if no appender has been created using that name. 10 | # 11 | def []( name ) @appenders[name] end 12 | 13 | # call-seq: 14 | # Appenders[name] = appender 15 | # 16 | # Stores the given _appender_ instance in the appender hash under the 17 | # key _name_. 18 | # 19 | def []=( name, value ) @appenders[name] = value end 20 | 21 | # call-seq: 22 | # Appenders.remove( name ) 23 | # 24 | # Removes the appender instance stored in the appender hash under the 25 | # key _name_. 26 | # 27 | def remove( name ) @appenders.delete(name) end 28 | 29 | # call-seq: 30 | # each {|appender| block} 31 | # 32 | # Yield each appender to the _block_. 33 | # 34 | def each( &block ) 35 | @appenders.values.each(&block) 36 | return nil 37 | end 38 | 39 | # :stopdoc: 40 | def reset 41 | @appenders.values.each {|appender| 42 | next if appender.nil? 43 | appender.close 44 | } 45 | @appenders.clear 46 | return nil 47 | end 48 | # :startdoc: 49 | 50 | extend self 51 | @appenders = Hash.new 52 | end # Appenders 53 | 54 | require libpath('logging/appenders/buffering') 55 | require libpath('logging/appenders/io') 56 | require libpath('logging/appenders/console') 57 | require libpath('logging/appenders/file') 58 | require libpath('logging/appenders/rolling_file') 59 | require libpath('logging/appenders/string_io') 60 | require libpath('logging/appenders/syslog') 61 | end # Logging 62 | 63 | -------------------------------------------------------------------------------- /examples/names.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # Loggers and appenders can be looked up by name. The bracket notation is 4 | # used to find these objects: 5 | # 6 | # Logging.logger['foo'] 7 | # Logging.appenders['bar'] 8 | # 9 | # A logger will be created if a new name is used. Appenders are different; 10 | # nil is returned when an unknown appender name is used. The reason for this 11 | # is that appenders come in many different flavors (so it is unclear which 12 | # type should be created), but there is only one type of logger. 13 | # 14 | # So it is useful to be able to create an appender and then reference it by 15 | # name to add it to multiple loggers. When the same name is used, the same 16 | # object will be returned by the bracket methods. 17 | # 18 | # Layouts do not have names. Some are stateful, and none are threadsafe. So 19 | # each appender is configured with it's own layout. 20 | # 21 | 22 | require 'logging' 23 | 24 | Logging.appenders.file('Debug File', :filename => 'debug.log') 25 | Logging.appenders.stderr('Standard Error', :level => :error) 26 | 27 | # configure the root logger 28 | Logging.logger.root.appenders = 'Debug File' 29 | Logging.logger.root.level = :debug 30 | 31 | # add the Standard Error appender to the Critical logger (it will use it's 32 | # own appender and the root logger's appender, too) 33 | Logging.logger['Critical'].appenders = 'Standard Error' 34 | 35 | # if you'll notice above, assigning appenders using just the name is valid 36 | # the logger is smart enough to figure out it was given a string and then 37 | # go lookup the appender by name 38 | 39 | # and now log some messages 40 | Logging.logger['Critical'].info 'just keeping you informed' 41 | Logging.logger['Critical'].fatal 'WTF!!' 42 | 43 | # :startdoc: 44 | -------------------------------------------------------------------------------- /examples/reusing_layouts.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # The formatting of log messages is controlled by the layout given to the 4 | # appender. By default all appenders use the Basic layout. It's pretty 5 | # basic. However, a more sophisticated Pattern layout can be used or one of 6 | # the Parseable layouts -- JSON or YAML. 7 | # 8 | # The available layouts are: 9 | # 10 | # Logging.layouts.basic 11 | # Logging.layouts.pattern 12 | # Logging.layouts.json 13 | # Logging.layouts.yaml 14 | # 15 | # After you configure a layout, you can reuse that layout among different 16 | # appenders if you so choose. This enables you to have some the style of log 17 | # output being sent to multiple destinations. 18 | # 19 | # We will store a Layout instance in a local variable, and then pass that 20 | # instance to each appender. 21 | # 22 | 23 | require 'logging' 24 | 25 | # create our pattern layout instance 26 | layout = Logging.layouts.pattern \ 27 | :pattern => '[%d] %-5l %c: %m\n', 28 | :date_pattern => '%Y-%m-%d %H:%M:%S' 29 | 30 | # only show "info" or higher messages on STDOUT using our layout 31 | Logging.appenders.stdout \ 32 | :level => :info, 33 | :layout => layout 34 | 35 | # send all log events to the development log (including debug) using our layout 36 | Logging.appenders.rolling_file \ 37 | 'development.log', 38 | :age => 'daily', 39 | :layout => layout 40 | 41 | log = Logging.logger['Foo::Bar'] 42 | log.add_appenders 'stdout', 'development.log' 43 | log.level = :debug 44 | 45 | log.debug "a very nice little debug message" 46 | log.info "things are operating normally" 47 | log.warn "this is your last warning" 48 | log.error StandardError.new("something went horribly wrong") 49 | log.fatal "I Die!" 50 | 51 | # :startdoc: 52 | -------------------------------------------------------------------------------- /test/performance.rb: -------------------------------------------------------------------------------- 1 | # 2 | # The peformance script is used to output a performance analysis page for the 3 | # Logging framework. You can run this script simply: 4 | # 5 | # ruby test/performance.rb 6 | # 7 | # This will write a file called "performance.html" that you can open in your web 8 | # browser. You will need the `ruby-prof` gem installed in order to run this 9 | # script. 10 | # ------------------------------------------------------------------------------ 11 | require 'rubygems' 12 | 13 | libpath = File.expand_path('../../lib', __FILE__) 14 | $:.unshift libpath 15 | require 'logging' 16 | 17 | begin 18 | gem 'log4r' 19 | require 'log4r' 20 | $log4r = true 21 | rescue LoadError 22 | $log4r = false 23 | end 24 | 25 | require 'logger' 26 | require 'ruby-prof' 27 | 28 | module Logging 29 | class Performance 30 | 31 | # number of iterations 32 | attr_reader :this_many 33 | 34 | # performance output file name 35 | attr_reader :output_file 36 | 37 | def initialize 38 | @this_many = 300_000 39 | @output_file = "performance.html" 40 | end 41 | 42 | def run 43 | pattern = Logging.layouts.pattern \ 44 | :pattern => '%.1l, [%d#%p] %5l -- %c: %m\n', 45 | :date_pattern => "%Y-%m-%dT%H:%M:%S.%s" 46 | 47 | Logging.appenders.string_io("sio", :layout => pattern) 48 | 49 | logger = ::Logging.logger["Performance"] 50 | logger.level = :warn 51 | logger.appenders = "sio" 52 | 53 | result = RubyProf.profile do 54 | this_many.times {logger.warn 'logged'} 55 | end 56 | 57 | printer = RubyProf::GraphHtmlPrinter.new(result) 58 | File.open(output_file, "w") { |fd| printer.print(fd) } 59 | end 60 | end 61 | end 62 | 63 | if __FILE__ == $0 64 | perf = Logging::Performance.new 65 | perf.run 66 | end 67 | -------------------------------------------------------------------------------- /lib/logging/root_logger.rb: -------------------------------------------------------------------------------- 1 | 2 | module Logging 3 | 4 | # The root logger exists to ensure that all loggers have a parent and a 5 | # defined logging level. If a logger is additive, eventually its log 6 | # events will propagate up to the root logger. 7 | # 8 | class RootLogger < Logger 9 | 10 | # undefine the methods that the root logger does not need 11 | %w(additive additive= parent parent=).each do |m| 12 | undef_method m.intern 13 | end 14 | 15 | attr_reader :level 16 | 17 | # call-seq: 18 | # RootLogger.new 19 | # 20 | # Returns a new root logger instance. This method will be called only 21 | # once when the +Repository+ singleton instance is created. 22 | # 23 | def initialize( ) 24 | ::Logging.init unless ::Logging.initialized? 25 | 26 | @name = 'root' 27 | @appenders = [] 28 | @additive = false 29 | @caller_tracing = false 30 | @level = 0 31 | ::Logging::Logger.define_log_methods(self) 32 | end 33 | 34 | # call-seq: 35 | # log <=> other 36 | # 37 | # Compares this logger by name to another logger. The normal return codes 38 | # for +String+ objects apply. 39 | # 40 | def <=>( other ) 41 | case other 42 | when self; 0 43 | when ::Logging::Logger; -1 44 | else raise ArgumentError, 'expecting a Logger instance' end 45 | end 46 | 47 | # call-seq: 48 | # level = :all 49 | # 50 | # Set the level for the root logger. The functionality of this method is 51 | # the same as +Logger#level=+, but setting the level to +nil+ for the 52 | # root logger is not allowed. The level is silently set to :all. 53 | # 54 | def level=( level ) 55 | super(level || 0) 56 | end 57 | 58 | end # class RootLogger 59 | end # module Logging 60 | 61 | -------------------------------------------------------------------------------- /examples/lazy.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # It happens sometimes that it is very expensive to construct a logging 4 | # message; for example, if a large object structure has to be traversed 5 | # during execution of an `object.to_s` method. It would be convenient to 6 | # delay creation of the message until the log event actually takes place. 7 | # 8 | # For example, with a logger configured only to show WARN messages and higher, 9 | # creating the log message for an INFO message would be wasteful. The INFO log 10 | # event would never be generated in this case. 11 | # 12 | # Log message creation can be performed lazily by wrapping the expensive 13 | # message generation code in a block and passing that to the logging method. 14 | 15 | require 'logging' 16 | 17 | Logging.logger.root.appenders = Logging.appenders.stdout 18 | Logging.logger.root.level = :info 19 | 20 | # We use this dummy method in order to see if the method gets called, but in practice, 21 | # this method might do complicated string operations to construct a log message. 22 | def expensive_method 23 | puts "Called!" 24 | "Expensive message" 25 | end 26 | 27 | log = Logging.logger['Lazy'] 28 | 29 | # If you log this message the usual way, expensive_method gets called before 30 | # debug, hence the Logging framework has no chance to stop it from being executed 31 | # immediately. 32 | log.info("Normal") 33 | log.debug(expensive_method) 34 | 35 | # If we put the message into a block, then the block is not executed, if 36 | # the message is not needed with the current log level. 37 | log.info("Block unused") 38 | log.debug { expensive_method } 39 | 40 | # If the log message is needed with the current log level, then the block is of 41 | # course executed and the log message appears as expected. 42 | log.info("Block used") 43 | log.warn { expensive_method } 44 | 45 | # :startdoc: 46 | -------------------------------------------------------------------------------- /lib/logging/log_event.rb: -------------------------------------------------------------------------------- 1 | 2 | module Logging 3 | 4 | # This class defines a logging event. 5 | class LogEvent 6 | # :stopdoc: 7 | 8 | # Regular expression used to parse out caller information 9 | # 10 | # * $1 == filename 11 | # * $2 == line number 12 | # * $3 == method name (might be nil) 13 | CALLER_RGXP = %r/([-\.\/\(\)\w]+):(\d+)(?::in `([^']+)')?/o 14 | #CALLER_INDEX = 2 15 | CALLER_INDEX = ((defined?(JRUBY_VERSION) && JRUBY_VERSION > '1.6' && JRUBY_VERSION < '9.0') || 16 | (defined?(RUBY_ENGINE) && RUBY_ENGINE[%r/^rbx/i])) ? 1 : 2 17 | # :startdoc: 18 | 19 | attr_accessor :logger, :level, :data, :time, :file, :line, :method_name 20 | 21 | # call-seq: 22 | # LogEvent.new( logger, level, [data], caller_tracing ) 23 | # 24 | # Creates a new log event with the given _logger_ name, numeric _level_, 25 | # array of _data_ from the user to be logged, and boolean _caller_tracing_ flag. 26 | # If the _caller_tracing_ flag is set to +true+ then Kernel::caller will be 27 | # invoked to get the execution trace of the logging method. 28 | # 29 | def initialize( logger, level, data, caller_tracing ) 30 | self.logger = logger 31 | self.level = level 32 | self.data = data 33 | self.time = Time.now.freeze 34 | 35 | if caller_tracing 36 | stack = Kernel.caller[CALLER_INDEX] 37 | return if stack.nil? 38 | 39 | match = CALLER_RGXP.match(stack) 40 | self.file = match[1] 41 | self.line = Integer(match[2]) 42 | self.method_name = match[3] unless match[3].nil? 43 | 44 | if (bp = ::Logging.basepath) && !bp.empty? && file.index(bp) == 0 45 | self.file = file.slice(bp.length + 1, file.length - bp.length) 46 | end 47 | else 48 | self.file = self.line = self.method_name = '' 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/test_utils.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('setup', File.dirname(__FILE__)) 3 | 4 | module TestLogging 5 | 6 | class TestUtils < Test::Unit::TestCase 7 | 8 | def test_string_shrink 9 | str = 'this is the foobar string' 10 | len = str.length 11 | 12 | r = str.shrink(len + 1) 13 | assert_same str, r 14 | 15 | r = str.shrink(len) 16 | assert_same str, r 17 | 18 | r = str.shrink(len - 1) 19 | assert_equal 'this is the...bar string', r 20 | 21 | r = str.shrink(len - 10) 22 | assert_equal 'this i...string', r 23 | 24 | r = str.shrink(4) 25 | assert_equal 't...', r 26 | 27 | r = str.shrink(3) 28 | assert_equal '...', r 29 | 30 | r = str.shrink(0) 31 | assert_equal '...', r 32 | 33 | assert_raises(ArgumentError) { str.shrink(-1) } 34 | 35 | r = str.shrink(len - 1, '##') 36 | assert_equal 'this is the##obar string', r 37 | 38 | r = str.shrink(len - 10, '##') 39 | assert_equal 'this is##string', r 40 | 41 | r = str.shrink(4, '##') 42 | assert_equal 't##g', r 43 | 44 | r = str.shrink(3, '##') 45 | assert_equal 't##', r 46 | 47 | r = str.shrink(0, '##') 48 | assert_equal '##', r 49 | end 50 | 51 | def test_logger_name 52 | assert_equal 'Array', Array.logger_name 53 | 54 | # some lines are commented out for compatibility with ruby 1.9 55 | 56 | c = Class.new(Array) 57 | # assert_equal '', c.name 58 | assert_equal 'Array', c.logger_name 59 | 60 | meta = class << Array; self; end 61 | # assert_equal '', meta.name 62 | assert_equal 'Array', meta.logger_name 63 | 64 | m = Module.new 65 | # assert_equal '', m.name 66 | assert_equal 'anonymous', m.logger_name 67 | 68 | c = Class.new(::Logging::Logger) 69 | # assert_equal '', c.name 70 | assert_equal 'Logging::Logger', c.logger_name 71 | 72 | meta = class << ::Logging::Logger; self; end 73 | # assert_equal '', meta.name 74 | assert_equal 'Logging::Logger', meta.logger_name 75 | end 76 | 77 | end # class TestUtils 78 | end # module TestLogging 79 | 80 | -------------------------------------------------------------------------------- /test/test_root_logger.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('setup', File.dirname(__FILE__)) 3 | 4 | module TestLogging 5 | 6 | class TestRootLogger < Test::Unit::TestCase 7 | include LoggingTestCase 8 | 9 | def setup 10 | super 11 | @root = ::Logging::Logger.root 12 | end 13 | 14 | def test_additive 15 | assert_raise(NoMethodError) {@root.additive} 16 | end 17 | 18 | def test_additive_eq 19 | assert_raise(NoMethodError) {@root.additive = true} 20 | end 21 | 22 | def test_level_eq 23 | assert_equal 0, @root.level 24 | 25 | assert_raise(ArgumentError) {@root.level = -1} 26 | assert_raise(ArgumentError) {@root.level = 6} 27 | assert_raise(ArgumentError) {@root.level = Object} 28 | assert_raise(ArgumentError) {@root.level = 'bob'} 29 | assert_raise(ArgumentError) {@root.level = :wtf} 30 | 31 | @root.level = 'INFO' 32 | assert_equal 1, @root.level 33 | 34 | @root.level = :warn 35 | assert_equal 2, @root.level 36 | 37 | @root.level = 'error' 38 | assert_equal 3, @root.level 39 | 40 | @root.level = 4 41 | assert_equal 4, @root.level 42 | 43 | @root.level = :all 44 | assert_equal 0, @root.level 45 | 46 | @root.level = 'OFF' 47 | assert_equal 5, @root.level 48 | 49 | @root.level = nil 50 | assert_equal 0, @root.level 51 | end 52 | 53 | def test_name 54 | assert_equal 'root', @root.name 55 | end 56 | 57 | def test_parent 58 | assert_raise(NoMethodError) {@root.parent} 59 | end 60 | 61 | def test_parent_eq 62 | assert_raise(NoMethodError) {@root.parent = nil} 63 | end 64 | 65 | def test_spaceship 66 | logs = %w( 67 | A A::B A::B::C A::B::C::D A::B::C::E A::B::C::E::G A::B::C::F 68 | ).map {|x| ::Logging::Logger[x]} 69 | 70 | logs.each do |log| 71 | assert_equal(-1, @root <=> log, "'root' <=> '#{log.name}'") 72 | end 73 | 74 | assert_equal 0, @root <=> @root 75 | assert_raise(ArgumentError) {@root <=> 'string'} 76 | end 77 | 78 | end # class TestRootLogger 79 | end # module TestLogging 80 | 81 | -------------------------------------------------------------------------------- /test/test_proxy.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('../setup', __FILE__) 3 | 4 | module TestLogging 5 | 6 | class TestProxy < Test::Unit::TestCase 7 | include LoggingTestCase 8 | 9 | def setup 10 | super 11 | 12 | ::Logging.init 13 | @appender = Logging.appenders.string_io('test_appender') 14 | logger = Logging.logger[Array] 15 | logger.level = :debug 16 | logger.appenders = @appender 17 | end 18 | 19 | def test_initialize 20 | ary = [] 21 | proxy = Logging::Proxy.new ary 22 | 23 | assert_instance_of Array, proxy 24 | 25 | proxy.concat [1,2,3] 26 | assert_equal 3, proxy.length 27 | assert_equal [1,2,3], ary 28 | end 29 | 30 | def test_method_logging 31 | proxy = Logging::Proxy.new [] 32 | assert_equal 0, proxy.length 33 | assert_equal "Array#length()\n", @appender.readline 34 | 35 | proxy.concat [1,2,3] 36 | assert_equal "Array#concat(#{[1,2,3].inspect})\n", @appender.readline 37 | 38 | proxy = Logging::Proxy.new Array 39 | proxy.name 40 | assert_equal "Array.name()\n", @appender.readline 41 | 42 | proxy.new 0 43 | assert_equal "Array.new(0)\n", @appender.readline 44 | end 45 | 46 | def test_custom_method_logging 47 | proxy = Logging::Proxy.new([]) { |name, *args, &block| 48 | @logger << "#@leader#{name}(#{args.inspect[1..-2]})" 49 | rv = @object.__send__(name, *args, &block) 50 | @logger << " => #{rv.inspect}\n" 51 | rv 52 | } 53 | @appender.clear 54 | 55 | assert_equal 0, proxy.length 56 | assert_equal "Array#length() => 0\n", @appender.readline 57 | 58 | proxy.concat [1,2,3] 59 | assert_equal "Array#concat(#{[1,2,3].inspect}) => #{[1,2,3].inspect}\n", @appender.readline 60 | 61 | proxy.concat [4,5,6] 62 | assert_equal "Array#concat(#{[4,5,6].inspect}) => #{[1,2,3,4,5,6].inspect}\n", @appender.readline 63 | end 64 | 65 | def test_error_when_proxying_nil 66 | assert_raises(ArgumentError, 'Cannot proxy nil') { 67 | Logging::Proxy.new nil 68 | } 69 | end 70 | 71 | end # TestProxy 72 | end # TestLogging 73 | 74 | -------------------------------------------------------------------------------- /test/test_log_event.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('setup', File.dirname(__FILE__)) 3 | 4 | module TestLogging 5 | 6 | class TestLogEvent < Test::Unit::TestCase 7 | include LoggingTestCase 8 | 9 | def setup 10 | super 11 | 12 | @appender = EventAppender.new('test') 13 | @logger = ::Logging::Logger['TestLogger'] 14 | @logger.add_appenders @appender 15 | 16 | @logger.info 'message 1' 17 | @event = @appender.event 18 | end 19 | 20 | def test_data 21 | assert_equal 'message 1', @event.data 22 | end 23 | 24 | def test_data_eq 25 | @event.data = 'message 2' 26 | assert_equal 'message 2', @event.data 27 | end 28 | 29 | def test_file 30 | assert_equal '', @event.file 31 | 32 | @logger.caller_tracing = true 33 | @logger.warn 'warning message' 34 | assert_match %r/test_log_event.rb\z/, @appender.event.file 35 | end 36 | 37 | def test_file_with_basepath 38 | ::Logging.basepath = File.expand_path("../../", __FILE__) 39 | 40 | @logger.caller_tracing = true 41 | @logger.warn "warning message" 42 | assert_equal "test/test_log_event.rb", @appender.event.file 43 | end 44 | 45 | def test_level 46 | assert_equal 1, @event.level 47 | end 48 | 49 | def test_level_eq 50 | @event.level = 3 51 | assert_equal 3, @event.level 52 | end 53 | 54 | def test_line 55 | assert_equal '', @event.file 56 | 57 | @logger.caller_tracing = true 58 | @logger.error 'error message' 59 | assert_equal __LINE__-1, @appender.event.line 60 | end 61 | 62 | def test_logger 63 | assert_equal 'TestLogger', @event.logger 64 | end 65 | 66 | def test_logger_eq 67 | @event.logger = 'MyLogger' 68 | assert_equal 'MyLogger', @event.logger 69 | end 70 | 71 | def test_method_name 72 | assert_equal '', @event.file 73 | 74 | @logger.caller_tracing = true 75 | @logger.debug 'debug message' 76 | assert_equal 'test_method_name', @appender.event.method_name 77 | end 78 | 79 | end # class TestLogEvent 80 | 81 | class EventAppender < ::Logging::Appender 82 | attr_reader :event 83 | def append( event ) @event = event end 84 | end 85 | 86 | end # module TestLogging 87 | 88 | -------------------------------------------------------------------------------- /examples/colorization.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # The Pattern layout can colorize log events based on a provided color scheme. 4 | # The configuration is a two part process. First the color scheme is defined 5 | # with the level colors and any pattern token colors. This color scheme is 6 | # then passed by name to the Pattern layout when it is created. 7 | # 8 | # The color scheme defines colors to be applied to the level token found in 9 | # the pattern layout. So that the "info" level will have one color, and the 10 | # "fatal" level will have a separate color. This applies only to the level 11 | # token in the Pattern layout. 12 | # 13 | # Common tokens can have their own color, too. The date token can be colored 14 | # blue, and the message token can be colored magenta. 15 | # 16 | # Colorization should only be applied to TTY logging destinations like STDOUT 17 | # and STDERR. Inserting color codes into a log file is generally considered 18 | # bad form; these color codes cause issues for some command line programs like 19 | # "less" and "more". 20 | # 21 | # A 'default" color scheme is provided with the Logging framework. In the 22 | # example below we create our own color scheme called 'bright' and apply it to 23 | # the 'stdout' appender. 24 | # 25 | 26 | require 'logging' 27 | 28 | # here we setup a color scheme called 'bright' 29 | Logging.color_scheme( 'bright', 30 | :levels => { 31 | :info => :green, 32 | :warn => :yellow, 33 | :error => :red, 34 | :fatal => [:white, :on_red] 35 | }, 36 | :date => :blue, 37 | :logger => :cyan, 38 | :message => :magenta 39 | ) 40 | 41 | Logging.appenders.stdout( 42 | 'stdout', 43 | :layout => Logging.layouts.pattern( 44 | :pattern => '[%d] %-5l %c: %m\n', 45 | :color_scheme => 'bright' 46 | ) 47 | ) 48 | 49 | log = Logging.logger['Happy::Colors'] 50 | log.add_appenders 'stdout' 51 | log.level = :debug 52 | 53 | # these log messages will be nicely colored 54 | # the level will be colored differently for each message 55 | # 56 | log.debug "a very nice little debug message" 57 | log.info "things are operating nominally" 58 | log.warn "this is your last warning" 59 | log.error StandardError.new("something went horribly wrong") 60 | log.fatal "I Die!" 61 | 62 | # :startdoc: 63 | -------------------------------------------------------------------------------- /lib/logging/proxy.rb: -------------------------------------------------------------------------------- 1 | 2 | module Logging 3 | 4 | # Defines a Proxy that will log all method calls on the proxied object. This 5 | # class uses +method_missing+ on a "blank slate" object to intercept all 6 | # method calls. The method name being called and the arguments are all 7 | # logged to the proxied object's logger instance. The log level and other 8 | # settings for the proxied object are honored by the Proxy instance. 9 | # 10 | # If you want, you can also supply your own +method_missing+ code as a block 11 | # to the constructor. 12 | # 13 | # Proxy.new(object) do |name, *args, &block| 14 | # # code to be executed before the proxied method 15 | # result = @object.send(name, *args, &block) 16 | # # code to be executed after the proxied method 17 | # result # <-- always return the result 18 | # end 19 | # 20 | # The proxied object is available as the "@object" variable. The logger is 21 | # available as the "@logger" variable. 22 | # 23 | class Proxy 24 | 25 | # :stopdoc: 26 | KEEPERS = %r/^__|^object_id$|^initialize$/ 27 | instance_methods(true).each { |m| undef_method m unless m[KEEPERS] } 28 | private_instance_methods(true).each { |m| undef_method m unless m[KEEPERS] } 29 | # :startdoc: 30 | 31 | # Create a new proxy for the given _object_. If an optional _block_ is 32 | # given it will be called before the proxied method. This _block_ will 33 | # replace the +method_missing+ implementation 34 | # 35 | def initialize( object, &block ) 36 | Kernel.raise ArgumentError, "Cannot proxy nil" if nil.equal? object 37 | 38 | @object = object 39 | @leader = @object.is_a?(Class) ? "#{@object.name}." : "#{@object.class.name}#" 40 | @logger = Logging.logger[object] 41 | 42 | if block 43 | eigenclass = class << self; self; end 44 | eigenclass.__send__(:define_method, :method_missing, &block) 45 | end 46 | end 47 | 48 | # All hail the magic of method missing. Here is where we are going to log 49 | # the method call and then forward to the proxied object. The return value 50 | # from the proxied object method call is passed back. 51 | # 52 | def method_missing( name, *args, &block ) 53 | @logger << "#@leader#{name}(#{args.inspect[1..-2]})\n" 54 | @object.send(name, *args, &block) 55 | end 56 | 57 | end # Proxy 58 | end # Logging 59 | 60 | -------------------------------------------------------------------------------- /lib/logging/appenders/string_io.rb: -------------------------------------------------------------------------------- 1 | 2 | module Logging::Appenders 3 | 4 | # Accessor / Factory for the StringIo appender. 5 | # 6 | def self.string_io( *args ) 7 | return ::Logging::Appenders::StringIo if args.empty? 8 | ::Logging::Appenders::StringIo.new(*args) 9 | end 10 | 11 | # This class provides an Appender that can write to a StringIO instance. 12 | # This is very useful for testing log message output. 13 | # 14 | class StringIo < ::Logging::Appenders::IO 15 | 16 | # The StringIO instance the appender is writing to. 17 | attr_reader :sio 18 | 19 | # call-seq: 20 | # StringIo.new( name, opts = {} ) 21 | # 22 | # Creates a new StringIo appender that will append log messages to a 23 | # StringIO instance. 24 | # 25 | def initialize( name, opts = {} ) 26 | @sio = StringIO.new 27 | @sio.extend IoToS 28 | @pos = 0 29 | super(name, @sio, opts) 30 | end 31 | 32 | # Reopen the underlying StringIO instance. If the instance is currently 33 | # closed then it will be opened. If the instance is currently open then it 34 | # will be closed and immediately opened. 35 | # 36 | def reopen 37 | @mutex.synchronize { 38 | if defined? @io and @io 39 | flush 40 | @io.close rescue nil 41 | end 42 | @io = @sio = StringIO.new 43 | @sio.extend IoToS 44 | @pos = 0 45 | } 46 | super 47 | self 48 | end 49 | 50 | # Clears the internal StringIO instance. All log messages are removed 51 | # from the buffer. 52 | # 53 | def clear 54 | @mutex.synchronize { 55 | @pos = 0 56 | @sio.seek 0 57 | @sio.truncate 0 58 | } 59 | end 60 | alias_method :reset, :clear 61 | 62 | %w[read readline readlines].each do|m| 63 | class_eval <<-CODE, __FILE__, __LINE__+1 64 | def #{m}( *args ) 65 | @mutex.synchronize { 66 | begin 67 | @sio.seek @pos 68 | rv = @sio.#{m}(*args) 69 | @pos = @sio.tell 70 | rv 71 | rescue EOFError 72 | nil 73 | end 74 | } 75 | end 76 | CODE 77 | end 78 | 79 | # :stopdoc: 80 | module IoToS 81 | def to_s 82 | seek 0 83 | str = read 84 | seek 0 85 | return str 86 | end 87 | end 88 | # :startdoc: 89 | 90 | end # StringIo 91 | end # Logging::Appenders 92 | 93 | -------------------------------------------------------------------------------- /examples/hierarchies.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # Loggers exist in a hierarchical relationship defined by their names. Each 4 | # logger has a parent (except for the root logger). A logger can zero or 5 | # more children. This parent/child relationship is determined by the Ruby 6 | # namespace separator '::'. 7 | # 8 | # root 9 | # |-- Foo 10 | # | |-- Foo::Bar 11 | # | `-- Foo::Baz 12 | # |-- ActiveRecord 13 | # | `-- ActiveRecord::Base 14 | # |-- ActiveSupport 15 | # | `-- ActiveSupport::Base 16 | # `-- Rails 17 | # 18 | # A logger inherits its log level from its parent. This level can be set for 19 | # each logger in the system. Setting the level on a logger affects all it's 20 | # children and grandchildren, etc. unless the child has it's own level set. 21 | # 22 | # Loggers also have a property called "additivity", and by default it is set 23 | # to true for all loggers. This property enables a logger to pass log events 24 | # up to its parent. 25 | # 26 | # If a logger does not have an appender and its additivity is true, it will 27 | # pass all log events up to its parent who will then try to send the log 28 | # event to its appenders. The parent will do the same thing, passing the log 29 | # event up the chain till the root logger is reached or some parent logger 30 | # has its additivity set to false. 31 | # 32 | # So, if the root logger is the only one with an appender, all loggers can 33 | # still output log events to the appender because of additivity. A logger 34 | # will ALWAYS send log events to its own appenders regardless of its 35 | # additivity. 36 | # 37 | # The show_configuration method can be used to dump the logging hierarchy. 38 | # 39 | 40 | require 'logging' 41 | 42 | Logging.logger.root.level = :debug 43 | 44 | foo = Logging.logger['Foo'] 45 | bar = Logging.logger['Foo::Bar'] 46 | baz = Logging.logger['Foo::Baz'] 47 | 48 | # configure the Foo logger 49 | foo.level = 'warn' 50 | foo.appenders = Logging.appenders.stdout 51 | 52 | # since Foo is the parent of Foo::Bar and Foo::Baz, these loggers all have 53 | # their level set to warn 54 | 55 | foo.warn 'this is a warning, not a ticket' 56 | bar.info 'this message will not be logged' 57 | baz.info 'nor will this message' 58 | bar.error 'but this error message will be logged' 59 | 60 | # let's demonstrate additivity of loggers 61 | 62 | Logging.logger.root.appenders = Logging.appenders.stdout 63 | 64 | baz.warn 'this message will be logged twice - once by Foo and once by root' 65 | 66 | foo.additive = false 67 | bar.warn "foo is no longer passing log events up to it's parent" 68 | 69 | # let's look at the logger hierarchy 70 | puts '='*76 71 | Logging.show_configuration 72 | 73 | # :startdoc: 74 | -------------------------------------------------------------------------------- /test/test_nested_diagnostic_context.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('../setup', __FILE__) 3 | 4 | module TestLogging 5 | 6 | class TestNestedDiagnosticContext < Test::Unit::TestCase 7 | include LoggingTestCase 8 | 9 | def test_push_pop 10 | ary = Logging.ndc.context 11 | assert ary.empty? 12 | 13 | assert_nil Logging.ndc.peek 14 | 15 | Logging.ndc.push 'first context' 16 | assert_equal 'first context', Logging.ndc.peek 17 | 18 | Logging.ndc << 'second' 19 | Logging.ndc << 'third' 20 | assert_equal 'third', Logging.ndc.peek 21 | assert_equal 3, ary.length 22 | 23 | assert_equal 'third', Logging.ndc.pop 24 | assert_equal 2, ary.length 25 | 26 | assert_equal 'second', Logging.ndc.pop 27 | assert_equal 1, ary.length 28 | 29 | assert_equal 'first context', Logging.ndc.pop 30 | assert ary.empty? 31 | end 32 | 33 | def test_push_block 34 | ary = Logging.ndc.context 35 | 36 | Logging.ndc.push('first context') do 37 | assert_equal 'first context', Logging.ndc.peek 38 | end 39 | assert ary.empty? 40 | 41 | Logging.ndc.push('first context') do 42 | assert_raise(ZeroDivisionError) do 43 | Logging.ndc.push('first context') { 1/0 } 44 | end 45 | end 46 | assert ary.empty? 47 | end 48 | 49 | def test_clear 50 | ary = Logging.ndc.context 51 | assert ary.empty? 52 | 53 | Logging.ndc << 'a' << 'b' << 'c' << 'd' 54 | assert_equal 'd', Logging.ndc.peek 55 | assert_equal 4, ary.length 56 | 57 | Logging.ndc.clear 58 | assert_nil Logging.ndc.peek 59 | end 60 | 61 | def test_thread_uniqueness 62 | Logging.ndc << 'first' << 'second' 63 | 64 | t = Thread.new { 65 | sleep 66 | 67 | Logging.ndc.clear 68 | assert_nil Logging.ndc.peek 69 | 70 | Logging.ndc << 42 71 | assert_equal 42, Logging.ndc.peek 72 | } 73 | 74 | Thread.pass until t.status == 'sleep' 75 | t.run 76 | t.join 77 | 78 | assert_equal 'second', Logging.ndc.peek 79 | end 80 | 81 | def test_thread_inheritance 82 | Logging.ndc << 'first' << 'second' 83 | 84 | t = Thread.new(Logging.ndc.context) { |ary| 85 | sleep 86 | 87 | assert_not_equal ary.object_id, Logging.ndc.context.object_id 88 | 89 | if Logging::INHERIT_CONTEXT 90 | assert_equal %w[first second], Logging.ndc.context 91 | else 92 | assert_empty Logging.ndc.context 93 | end 94 | } 95 | 96 | Thread.pass until t.status == 'sleep' 97 | Logging.ndc << 'third' 98 | 99 | t.run 100 | t.join 101 | end 102 | end # class TestNestedDiagnosticContext 103 | end # module TestLogging 104 | -------------------------------------------------------------------------------- /lib/logging/appenders/console.rb: -------------------------------------------------------------------------------- 1 | module Logging::Appenders 2 | 3 | # This class is provides an Appender base class for writing to the standard IO 4 | # stream - STDOUT and STDERR. This class should not be instantiated directly. 5 | # The `Stdout` and `Stderr` subclasses should be used. 6 | class Console < ::Logging::Appenders::IO 7 | 8 | # call-seq: 9 | # Stdout.new( name = 'stdout' ) 10 | # Stderr.new( :layout => layout ) 11 | # Stdout.new( name = 'stdout', :level => 'info' ) 12 | # 13 | # Creates a new Stdout/Stderr Appender. The name 'stdout'/'stderr' will be 14 | # used unless another is given. Optionally, a layout can be given for the 15 | # appender to use (otherwise a basic appender will be created) and a log 16 | # level can be specified. 17 | # 18 | # Options: 19 | # 20 | # :layout => the layout to use when formatting log events 21 | # :level => the level at which to log 22 | # 23 | def initialize( *args ) 24 | name = self.class.name.split("::").last.downcase 25 | 26 | opts = args.last.is_a?(Hash) ? args.pop : {} 27 | name = args.shift unless args.empty? 28 | 29 | io = open_fd 30 | opts[:encoding] = io.external_encoding 31 | 32 | super(name, io, opts) 33 | end 34 | 35 | # Reopen the connection to the underlying logging destination. If the 36 | # connection is currently closed then it will be opened. If the connection 37 | # is currently open then it will be closed and immediately reopened. 38 | def reopen 39 | @mutex.synchronize { 40 | flush if defined? @io && @io 41 | @io = open_fd 42 | } 43 | super 44 | self 45 | end 46 | 47 | private 48 | 49 | def open_fd 50 | case self.class.name 51 | when "Logging::Appenders::Stdout"; STDOUT 52 | when "Logging::Appenders::Stderr"; STDERR 53 | else 54 | raise RuntimeError, "Please do not use the `Logging::Appenders::Console` class directly - " + 55 | "use `Logging::Appenders::Stdout` and `Logging::Appenders::Stderr` instead" + 56 | " [class #{self.class.name}]" 57 | end 58 | end 59 | end 60 | 61 | # This class provides an Appender that can write to STDOUT. 62 | Stdout = Class.new(Console) 63 | 64 | # This class provides an Appender that can write to STDERR. 65 | Stderr = Class.new(Console) 66 | 67 | # Accessor / Factory for the Stdout appender. 68 | def self.stdout( *args ) 69 | if args.empty? 70 | return self['stdout'] || ::Logging::Appenders::Stdout.new 71 | end 72 | ::Logging::Appenders::Stdout.new(*args) 73 | end 74 | 75 | # Accessor / Factory for the Stderr appender. 76 | def self.stderr( *args ) 77 | if args.empty? 78 | return self['stderr'] || ::Logging::Appenders::Stderr.new 79 | end 80 | ::Logging::Appenders::Stderr.new(*args) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /examples/mdc.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | # 3 | # A diagnostic context allows us to attach state information to every log 4 | # message. This is useful for applications that serve client requests - 5 | # information about the client can be included in the log messages for the 6 | # duration of the request processing. This allows you to identify related log 7 | # messages in concurrent system. 8 | # 9 | # The Mapped Diagnostic Context tracks state information in a collection of 10 | # key/value pairs. In this example we are creating a few threads that will log 11 | # quotes from famous people. Each thread has its own diagnostic context 12 | # containing the name of the famous person. 13 | # 14 | # Our PatternLayout is configured to attach the "first" and the "last" name of 15 | # our famous person to each log message. 16 | # 17 | 18 | require 'logging' 19 | 20 | # log the first and last names of the celebrity with each quote 21 | Logging.appenders.stdout( 22 | :layout => Logging.layouts.pattern(:pattern => '%X{first} %X{last}: %m\n') 23 | ) 24 | 25 | log = Logging.logger['User'] 26 | log.add_appenders 'stdout' 27 | log.level = :debug 28 | 29 | Logging.mdc['first'] = 'John' 30 | Logging.mdc['last'] = 'Doe' 31 | 32 | # in this first thread we will log some quotes by Alan Rickman 33 | t1 = Thread.new { 34 | Logging.mdc['first'] = 'Alan' 35 | Logging.mdc['last'] = 'Rickman' 36 | 37 | [ %q{I've never been able to plan my life. I just lurch from indecision to indecision.}, 38 | %q{If only life could be a little more tender and art a little more robust.}, 39 | %q{I do take my work seriously and the way to do that is not to take yourself too seriously.}, 40 | %q{I'm a quite serious actor who doesn't mind being ridiculously comic.} 41 | ].each { |quote| 42 | sleep rand 43 | log.info quote 44 | } 45 | } 46 | 47 | # in this second thread we will log some quotes by William Butler Yeats 48 | t2 = Thread.new { 49 | Logging.mdc['first'] = 'William' 50 | Logging.mdc['middle'] = 'Butler' 51 | Logging.mdc['last'] = 'Yeats' 52 | 53 | [ %q{Tread softly because you tread on my dreams.}, 54 | %q{The best lack all conviction, while the worst are full of passionate intensity.}, 55 | %q{Education is not the filling of a pail, but the lighting of a fire.}, 56 | %q{Do not wait to strike till the iron is hot; but make it hot by striking.}, 57 | %q{People who lean on logic and philosophy and rational exposition end by starving the best part of the mind.} 58 | ].each { |quote| 59 | sleep rand 60 | log.info quote 61 | } 62 | } 63 | 64 | # and in this third thread we will log some quotes by Bono 65 | t3 = Thread.new { 66 | Logging.mdc.clear # otherwise we inherit the last name "Doe" 67 | Logging.mdc['first'] = 'Bono' 68 | 69 | [ %q{Music can change the world because it can change people.}, 70 | %q{The less you know, the more you believe.} 71 | ].each { |quote| 72 | sleep rand 73 | log.info quote 74 | } 75 | } 76 | 77 | t1.join 78 | t2.join 79 | t3.join 80 | 81 | log.info %q{and now we are done} 82 | 83 | # :startdoc: 84 | -------------------------------------------------------------------------------- /lib/logging/appenders/io.rb: -------------------------------------------------------------------------------- 1 | 2 | module Logging::Appenders 3 | 4 | # Accessor / Factory for the IO appender. 5 | def self.io( *args ) 6 | return ::Logging::Appenders::IO if args.empty? 7 | ::Logging::Appenders::IO.new(*args) 8 | end 9 | 10 | # This class provides an Appender that can write to any IO stream 11 | # configured for writing. 12 | class IO < ::Logging::Appender 13 | include Buffering 14 | 15 | # The method that will be used to close the IO stream. Defaults to :close 16 | # but can be :close_read, :close_write or nil. When nil, the IO stream 17 | # will not be closed when the appender's close method is called. 18 | attr_accessor :close_method 19 | 20 | # call-seq: 21 | # IO.new( name, io ) 22 | # IO.new( name, io, :layout => layout ) 23 | # 24 | # Creates a new IO Appender using the given name that will use the _io_ 25 | # stream as the logging destination. 26 | def initialize( name, io, opts = {} ) 27 | unless io.respond_to? :write 28 | raise TypeError, "expecting an IO object but got '#{io.class.name}'" 29 | end 30 | 31 | @io = io 32 | @io.sync = true if io.respond_to? :sync= 33 | @close_method = :close 34 | 35 | super(name, opts) 36 | configure_buffering(opts) 37 | end 38 | 39 | # call-seq: 40 | # close( footer = true ) 41 | # 42 | # Close the appender and writes the layout footer to the logging 43 | # destination if the _footer_ flag is set to +true+. Log events will 44 | # no longer be written to the logging destination after the appender 45 | # is closed. 46 | def close( *args ) 47 | return self if @io.nil? 48 | super 49 | 50 | io, @io = @io, nil 51 | if ![STDIN, STDERR, STDOUT].include?(io) 52 | io.send(@close_method) if @close_method && io.respond_to?(@close_method) 53 | end 54 | rescue IOError 55 | ensure 56 | return self 57 | end 58 | 59 | # Reopen the connection to the underlying logging destination. If the 60 | # connection is currently closed then it will be opened. If the connection 61 | # is currently open then it will be closed and immediately opened. If 62 | # supported, the IO will have its sync mode set to `true` so that all writes 63 | # are immediately flushed to the underlying operating system. 64 | def reopen 65 | super 66 | @io.sync = true if @io.respond_to? :sync= 67 | self 68 | end 69 | 70 | private 71 | 72 | # This method is called by the buffering code when messages need to be 73 | # written to the logging destination. 74 | def canonical_write( str ) 75 | return self if @io.nil? 76 | str = str.force_encoding(encoding) if encoding && str.encoding != encoding 77 | @mutex.synchronize { @io.write str } 78 | self 79 | rescue StandardError => err 80 | handle_internal_error(err) 81 | end 82 | 83 | def handle_internal_error( err ) 84 | return err if off? 85 | self.level = :off 86 | ::Logging.log_internal {"appender #{name.inspect} has been disabled"} 87 | ::Logging.log_internal_error(err) 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/appenders/test_console.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('../setup', File.dirname(__FILE__)) 3 | 4 | module TestLogging 5 | module TestAppenders 6 | 7 | class TestConsole < Test::Unit::TestCase 8 | include LoggingTestCase 9 | 10 | def test_initialize 11 | assert_raise(RuntimeError) { Logging::Appenders::Console.new("test") } 12 | end 13 | end 14 | 15 | class TestStdout < Test::Unit::TestCase 16 | include LoggingTestCase 17 | 18 | def test_initialize 19 | Logging::Repository.instance 20 | 21 | appender = Logging.appenders.stdout 22 | assert_equal 'stdout', appender.name 23 | 24 | io = appender.instance_variable_get(:@io) 25 | assert_same STDOUT, io 26 | assert_equal STDOUT.fileno, io.fileno 27 | 28 | appender = Logging.appenders.stdout('foo') 29 | assert_equal 'foo', appender.name 30 | 31 | appender = Logging.appenders.stdout(:level => :warn) 32 | assert_equal 'stdout', appender.name 33 | assert_equal 2, appender.level 34 | 35 | appender = Logging.appenders.stdout('bar', :level => :error) 36 | assert_equal 'bar', appender.name 37 | assert_equal 3, appender.level 38 | end 39 | 40 | def test_reopen 41 | Logging::Repository.instance 42 | 43 | appender = Logging.appenders.stdout 44 | io = appender.instance_variable_get(:@io) 45 | 46 | appender.close 47 | assert appender.closed? 48 | refute io.closed? 49 | refute STDOUT.closed? 50 | 51 | appender.reopen 52 | refute appender.closed? 53 | 54 | new_io = appender.instance_variable_get(:@io) 55 | assert_same io, new_io 56 | refute new_io.closed? 57 | refute io.closed? 58 | end 59 | end 60 | 61 | class TestStderr < Test::Unit::TestCase 62 | include LoggingTestCase 63 | 64 | def test_initialize 65 | Logging::Repository.instance 66 | 67 | appender = Logging.appenders.stderr 68 | assert_equal 'stderr', appender.name 69 | 70 | io = appender.instance_variable_get(:@io) 71 | assert_same STDERR, io 72 | assert_equal STDERR.fileno, io.fileno 73 | 74 | appender = Logging.appenders.stderr('foo') 75 | assert_equal 'foo', appender.name 76 | 77 | appender = Logging.appenders.stderr(:level => :warn) 78 | assert_equal 'stderr', appender.name 79 | assert_equal 2, appender.level 80 | 81 | appender = Logging.appenders.stderr('bar', :level => :error) 82 | assert_equal 'bar', appender.name 83 | assert_equal 3, appender.level 84 | end 85 | 86 | def test_reopen 87 | Logging::Repository.instance 88 | 89 | appender = Logging.appenders.stderr 90 | io = appender.instance_variable_get(:@io) 91 | 92 | appender.close 93 | assert appender.closed? 94 | refute io.closed? 95 | refute STDERR.closed? 96 | 97 | appender.reopen 98 | refute appender.closed? 99 | 100 | new_io = appender.instance_variable_get(:@io) 101 | assert_same io, new_io 102 | refute new_io.closed? 103 | refute io.closed? 104 | end 105 | end 106 | end 107 | end 108 | 109 | -------------------------------------------------------------------------------- /lib/logging/appenders/file.rb: -------------------------------------------------------------------------------- 1 | 2 | module Logging::Appenders 3 | 4 | # Accessor / Factory for the File appender. 5 | def self.file( *args ) 6 | fail ArgumentError, '::Logging::Appenders::File needs a name as first argument.' if args.empty? 7 | ::Logging::Appenders::File.new(*args) 8 | end 9 | 10 | # This class provides an Appender that can write to a File. 11 | class File < ::Logging::Appenders::IO 12 | 13 | # call-seq: 14 | # File.assert_valid_logfile( filename ) => true 15 | # 16 | # Asserts that the given _filename_ can be used as a log file by ensuring 17 | # that if the file exists it is a regular file and it is writable. If 18 | # the file does not exist, then the directory is checked to see if it is 19 | # writable. 20 | # 21 | # An +ArgumentError+ is raised if any of these assertions fail. 22 | def self.assert_valid_logfile( fn ) 23 | if ::File.exist?(fn) 24 | if !::File.file?(fn) 25 | raise ArgumentError, "#{fn} is not a regular file" 26 | elsif !::File.writable?(fn) 27 | raise ArgumentError, "#{fn} is not writeable" 28 | end 29 | elsif !::File.writable?(::File.dirname(fn)) 30 | raise ArgumentError, "#{::File.dirname(fn)} is not writable" 31 | end 32 | true 33 | end 34 | 35 | # call-seq: 36 | # File.new( name, :filename => 'file' ) 37 | # File.new( name, :filename => 'file', :truncate => true ) 38 | # File.new( name, :filename => 'file', :layout => layout ) 39 | # 40 | # Creates a new File Appender that will use the given filename as the 41 | # logging destination. If the file does not already exist it will be 42 | # created. If the :truncate option is set to +true+ then the file will 43 | # be truncated before writing begins; otherwise, log messages will be 44 | # appended to the file. 45 | def initialize( name, opts = {} ) 46 | @filename = opts.fetch(:filename, name) 47 | raise ArgumentError, 'no filename was given' if @filename.nil? 48 | 49 | @filename = ::File.expand_path(@filename).freeze 50 | self.class.assert_valid_logfile(@filename) 51 | 52 | self.encoding = opts.fetch(:encoding, self.encoding) 53 | 54 | io = open_file 55 | super(name, io, opts) 56 | 57 | truncate if opts.fetch(:truncate, false) 58 | end 59 | 60 | # Returns the path to the logfile. 61 | attr_reader :filename 62 | 63 | # Reopen the connection to the underlying logging destination. If the 64 | # connection is currently closed then it will be opened. If the connection 65 | # is currently open then it will be closed and immediately opened. 66 | def reopen 67 | @mutex.synchronize { 68 | if defined? @io && @io 69 | flush 70 | @io.close rescue nil 71 | end 72 | @io = open_file 73 | } 74 | super 75 | self 76 | end 77 | 78 | 79 | protected 80 | 81 | def truncate 82 | @mutex.synchronize { 83 | begin 84 | @io.flock(::File::LOCK_EX) 85 | @io.truncate(0) 86 | ensure 87 | @io.flock(::File::LOCK_UN) 88 | end 89 | } 90 | end 91 | 92 | def open_file 93 | mode = ::File::WRONLY | ::File::APPEND 94 | ::File.open(filename, mode: mode, external_encoding: encoding) 95 | rescue Errno::ENOENT 96 | create_file 97 | end 98 | 99 | def create_file 100 | mode = ::File::WRONLY | ::File::APPEND | ::File::CREAT | ::File::EXCL 101 | ::File.open(filename, mode: mode, external_encoding: encoding) 102 | rescue Errno::EEXIST 103 | open_file 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /test/layouts/test_color_pattern.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('../../setup', __FILE__) 3 | 4 | module TestLogging 5 | module TestLayouts 6 | 7 | class TestColorPattern < Test::Unit::TestCase 8 | include LoggingTestCase 9 | CS = ::Logging::ColorScheme 10 | 11 | def setup 12 | super 13 | 14 | ::Logging.color_scheme :levels, :levels => { 15 | :debug => :blue, :info => :green, :warn => :yellow, :error => :red, :fatal => :cyan 16 | } 17 | 18 | ::Logging.color_scheme :lines, :lines => { 19 | :debug => :blue, :info => :green, :warn => :yellow, :error => :red, :fatal => :cyan 20 | }, :date => :blue, :logger => :cyan 21 | 22 | ::Logging.color_scheme :tokens, :date => :blue, :logger => :green, :message => :magenta 23 | 24 | @levels = Logging::LEVELS 25 | end 26 | 27 | def test_level_coloring 28 | layout = Logging.layouts.pattern(:color_scheme => :levels) 29 | event = Logging::LogEvent.new('ArrayLogger', @levels['info'], 'log message', false) 30 | 31 | rgxp = Regexp.new(Regexp.escape("#{CS::GREEN}INFO #{CS::RESET}")) 32 | assert_match rgxp, layout.format(event) 33 | 34 | event.level = @levels['debug'] 35 | rgxp = Regexp.new(Regexp.escape("#{CS::BLUE}DEBUG#{CS::RESET}")) 36 | assert_match rgxp, layout.format(event) 37 | 38 | event.level = @levels['error'] 39 | rgxp = Regexp.new(Regexp.escape("#{CS::RED}ERROR#{CS::RESET}")) 40 | assert_match rgxp, layout.format(event) 41 | end 42 | 43 | def test_multiple_level_coloring 44 | layout = Logging.layouts.pattern(:pattern => '%.1l, %5l -- %c: %m\n', :color_scheme => :levels) 45 | event = Logging::LogEvent.new('ArrayLogger', @levels['info'], 'log message', false) 46 | 47 | rgxp = Regexp.new(Regexp.escape("#{CS::GREEN}I#{CS::RESET}, #{CS::GREEN} INFO#{CS::RESET}")) 48 | assert_match rgxp, layout.format(event) 49 | 50 | event.level = @levels['debug'] 51 | rgxp = Regexp.new(Regexp.escape("#{CS::BLUE}D#{CS::RESET}, #{CS::BLUE}DEBUG#{CS::RESET}")) 52 | assert_match rgxp, layout.format(event) 53 | 54 | event.level = @levels['error'] 55 | rgxp = Regexp.new(Regexp.escape("#{CS::RED}E#{CS::RESET}, #{CS::RED}ERROR#{CS::RESET}")) 56 | assert_match rgxp, layout.format(event) 57 | end 58 | 59 | def test_line_coloring 60 | layout = Logging.layouts.pattern(:color_scheme => :lines) 61 | event = Logging::LogEvent.new('ArrayLogger', @levels['info'], 'log message', false) 62 | 63 | rgxp = Regexp.new('^'+Regexp.escape(CS::GREEN)+'.*?'+Regexp.escape(CS::RESET)+'$', Regexp::MULTILINE) 64 | assert_match rgxp, layout.format(event) 65 | 66 | event.level = @levels['error'] 67 | rgxp = Regexp.new('^'+Regexp.escape(CS::RED)+'.*?'+Regexp.escape(CS::RESET)+'$', Regexp::MULTILINE) 68 | assert_match rgxp, layout.format(event) 69 | 70 | event.level = @levels['warn'] 71 | rgxp = Regexp.new('^'+Regexp.escape(CS::YELLOW)+'.*?'+Regexp.escape(CS::RESET)+'$', Regexp::MULTILINE) 72 | assert_match rgxp, layout.format(event) 73 | end 74 | 75 | def test_token_coloring 76 | layout = Logging.layouts.pattern(:color_scheme => :tokens) 77 | event = Logging::LogEvent.new('ArrayLogger', @levels['info'], 'log message', false) 78 | 79 | rgxp = Regexp.new( 80 | '^\['+Regexp.escape(CS::BLUE)+'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}'+Regexp.escape(CS::RESET)+ 81 | '\] INFO -- '+Regexp.escape(CS::GREEN)+'ArrayLogger'+Regexp.escape(CS::RESET)+ 82 | ' : '+Regexp.escape(CS::MAGENTA)+'log message'+Regexp.escape(CS::RESET) 83 | ) 84 | assert_match rgxp, layout.format(event) 85 | end 86 | 87 | end # TestColorPattern 88 | end # TestLayouts 89 | end # TestLogging 90 | 91 | -------------------------------------------------------------------------------- /test/benchmark.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | libpath = File.expand_path('../../lib', __FILE__) 4 | $:.unshift libpath 5 | require 'logging' 6 | 7 | begin 8 | gem 'log4r' 9 | require 'log4r' 10 | $log4r = true 11 | rescue LoadError 12 | $log4r = false 13 | end 14 | 15 | require 'benchmark' 16 | require 'logger' 17 | 18 | module Logging 19 | class Benchmark 20 | 21 | def run 22 | this_many = 300_000 23 | 24 | pattern = Logging.layouts.pattern \ 25 | :pattern => '%.1l, [%d #%p] %5l -- %c: %m\n', 26 | :date_pattern => "%Y-%m-%dT%H:%M:%S.%s" 27 | 28 | Logging.appenders.string_io('sio', :layout => pattern) 29 | sio = Logging.appenders['sio'].sio 30 | 31 | logging = ::Logging.logger['benchmark'] 32 | logging.level = :warn 33 | logging.appenders = 'sio' 34 | 35 | logger = ::Logger.new sio 36 | logger.level = ::Logger::WARN 37 | 38 | log4r = if $log4r 39 | l4r = ::Log4r::Logger.new('benchmark') 40 | l4r.level = ::Log4r::WARN 41 | l4r.add ::Log4r::IOOutputter.new( 42 | 'benchmark', sio, 43 | :formatter => ::Log4r::PatternFormatter.new( 44 | :pattern => "%.1l, [%d #\#{Process.pid}] %5l : %M\n", 45 | :date_pattern => "%Y-%m-%dT%H:%M:%S.%6N" 46 | ) 47 | ) 48 | l4r 49 | end 50 | 51 | puts "== Debug (not logged) ==\n" 52 | ::Benchmark.bm(10) do |bm| 53 | bm.report('Logging:') {this_many.times {logging.debug 'not logged'}} 54 | bm.report('Logger:') {this_many.times {logger.debug 'not logged'}} 55 | bm.report('Log4r:') {this_many.times {log4r.debug 'not logged'}} if log4r 56 | end 57 | 58 | puts "\n== Warn (logged) ==\n" 59 | ::Benchmark.bm(10) do |bm| 60 | sio.seek 0 61 | bm.report('Logging:') {this_many.times {logging.warn 'logged'}} 62 | sio.seek 0 63 | bm.report('Logger:') {this_many.times {logger.warn 'logged'}} 64 | sio.seek 0 65 | bm.report('Log4r:') {this_many.times {log4r.warn 'logged'}} if log4r 66 | end 67 | 68 | puts "\n== Concat ==\n" 69 | ::Benchmark.bm(10) do |bm| 70 | sio.seek 0 71 | bm.report('Logging:') {this_many.times {logging << 'logged'}} 72 | sio.seek 0 73 | bm.report('Logger:') {this_many.times {logger << 'logged'}} 74 | puts "Log4r: not supported" if log4r 75 | end 76 | 77 | write_size = 250 78 | auto_flushing_size = 500 79 | 80 | logging_async = ::Logging.logger['AsyncFile'] 81 | logging_async.level = :info 82 | logging_async.appenders = Logging.appenders.file \ 83 | 'benchmark_async.log', 84 | :layout => pattern, 85 | :write_size => write_size, 86 | :auto_flushing => auto_flushing_size, 87 | :async => true 88 | 89 | logging_sync = ::Logging.logger['SyncFile'] 90 | logging_sync.level = :info 91 | logging_sync.appenders = Logging.appenders.file \ 92 | 'benchmark_sync.log', 93 | :layout => pattern, 94 | :write_size => write_size, 95 | :auto_flushing => auto_flushing_size, 96 | :async => false 97 | 98 | puts "\n== File ==\n" 99 | ::Benchmark.bm(20) do |bm| 100 | bm.report('Logging (Async):') {this_many.times { |n| logging_async.info "Iteration #{n}"}} 101 | bm.report('Logging (Sync):') {this_many.times { |n| logging_sync.info "Iteration #{n}"}} 102 | end 103 | 104 | File.delete('benchmark_async.log') 105 | File.delete('benchmark_sync.log') 106 | end 107 | end 108 | end 109 | 110 | if __FILE__ == $0 111 | bm = ::Logging::Benchmark.new 112 | bm.run 113 | end 114 | -------------------------------------------------------------------------------- /test/layouts/test_nested_exceptions.rb: -------------------------------------------------------------------------------- 1 | 2 | require_relative '../setup' 3 | 4 | module TestLogging 5 | module TestLayouts 6 | class TestNestedExceptions < Test::Unit::TestCase 7 | include LoggingTestCase 8 | 9 | def test_basic_format_obj 10 | err = nil 11 | begin 12 | begin 13 | raise ArgumentError, 'nested exception' 14 | rescue 15 | raise StandardError, 'root exception' 16 | end 17 | rescue => e 18 | err = e 19 | end 20 | 21 | layout = Logging.layouts.basic({}) 22 | log = layout.format_obj(err) 23 | assert_not_nil log.index(' root exception') 24 | 25 | if err.respond_to?(:cause) 26 | assert_not_nil log.index(' nested exception') 27 | assert(log.index(' root exception') < log.index(' nested exception')) 28 | end 29 | end 30 | 31 | def test_cause_depth_limiting 32 | err = nil 33 | begin 34 | begin 35 | begin 36 | raise TypeError, 'nested exception 2' 37 | rescue 38 | raise ArgumentError, 'nested exception 1' 39 | end 40 | rescue 41 | raise StandardError, 'root exception' 42 | end 43 | rescue => e 44 | err = e 45 | end 46 | 47 | layout = Logging.layouts.basic(cause_depth: 1) 48 | log = layout.format_obj(err) 49 | assert_not_nil log.index(' root exception') 50 | 51 | if err.respond_to?(:cause) 52 | assert_not_nil log.index(' nested exception 1') 53 | assert_nil log.index(' nested exception 2') 54 | assert_equal '--- Further #cause backtraces were omitted ---', log.split("\n\t").last 55 | end 56 | end 57 | 58 | def test_parseable_format_obj 59 | err = nil 60 | begin 61 | begin 62 | raise ArgumentError, 'nested exception' 63 | rescue 64 | raise StandardError, 'root exception' 65 | end 66 | rescue => e 67 | err = e 68 | end 69 | 70 | layout = Logging.layouts.parseable.new 71 | log = layout.format_obj(err) 72 | assert_equal 'StandardError', log[:class] 73 | assert_equal 'root exception', log[:message] 74 | assert log[:backtrace].size > 0 75 | 76 | if err.respond_to?(:cause) 77 | assert_not_nil log[:cause] 78 | 79 | log = log[:cause] 80 | assert_equal 'ArgumentError', log[:class] 81 | assert_equal 'nested exception', log[:message] 82 | assert_nil log[:cause] 83 | assert log[:backtrace].size > 0 84 | end 85 | end 86 | 87 | def test_parseable_cause_depth_limiting 88 | err = nil 89 | begin 90 | begin 91 | begin 92 | raise TypeError, 'nested exception 2' 93 | rescue 94 | raise ArgumentError, 'nested exception 1' 95 | end 96 | rescue 97 | raise StandardError, 'root exception' 98 | end 99 | rescue => e 100 | err = e 101 | end 102 | 103 | layout = Logging.layouts.parseable.new(cause_depth: 1) 104 | log = layout.format_obj(err) 105 | 106 | assert_equal 'StandardError', log[:class] 107 | assert_equal 'root exception', log[:message] 108 | assert log[:backtrace].size > 0 109 | 110 | if err.respond_to?(:cause) 111 | assert_not_nil log[:cause] 112 | 113 | log = log[:cause] 114 | assert_equal 'ArgumentError', log[:class] 115 | assert_equal 'nested exception 1', log[:message] 116 | assert_equal({message: "Further #cause backtraces were omitted"}, log[:cause]) 117 | assert log[:backtrace].size > 0 118 | end 119 | end 120 | end 121 | end 122 | end 123 | 124 | require 'pp' 125 | -------------------------------------------------------------------------------- /test/test_mapped_diagnostic_context.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('../setup', __FILE__) 3 | 4 | module TestLogging 5 | 6 | class TestMappedDiagnosticContext < Test::Unit::TestCase 7 | include LoggingTestCase 8 | 9 | def test_key_value_access 10 | assert_nil Logging.mdc['foo'] 11 | 12 | Logging.mdc['foo'] = 'bar' 13 | assert_equal 'bar', Logging.mdc[:foo] 14 | assert_same Logging.mdc['foo'], Logging.mdc[:foo] 15 | 16 | Logging.mdc.delete(:foo) 17 | assert_nil Logging.mdc['foo'] 18 | end 19 | 20 | def test_clear 21 | Logging.mdc['foo'] = 'bar' 22 | Logging.mdc['baz'] = 'buz' 23 | 24 | assert_equal 'bar', Logging.mdc[:foo] 25 | assert_equal 'buz', Logging.mdc[:baz] 26 | 27 | Logging.mdc.clear 28 | 29 | assert_nil Logging.mdc['foo'] 30 | assert_nil Logging.mdc['baz'] 31 | end 32 | 33 | def test_context_update 34 | Logging.mdc.update(:foo => 'bar', :baz => 'buz') 35 | assert_equal 'bar', Logging.mdc[:foo] 36 | assert_equal 'buz', Logging.mdc[:baz] 37 | 38 | Logging.mdc.update('foo' => 1, 'baz' => 2) 39 | assert_equal 1, Logging.mdc[:foo] 40 | assert_equal 2, Logging.mdc[:baz] 41 | 42 | assert_equal 1, Logging.mdc.stack.length 43 | end 44 | 45 | def test_context_pushing 46 | assert Logging.mdc.context.empty? 47 | assert_equal 1, Logging.mdc.stack.length 48 | 49 | Logging.mdc.push(:foo => 'bar', :baz => 'buz') 50 | assert_equal 'bar', Logging.mdc[:foo] 51 | assert_equal 'buz', Logging.mdc[:baz] 52 | assert_equal 2, Logging.mdc.stack.length 53 | 54 | Logging.mdc.push(:foo => 1, :baz => 2, :foobar => 3) 55 | assert_equal 1, Logging.mdc[:foo] 56 | assert_equal 2, Logging.mdc[:baz] 57 | assert_equal 3, Logging.mdc[:foobar] 58 | assert_equal 3, Logging.mdc.stack.length 59 | 60 | Logging.mdc.pop 61 | assert_equal 'bar', Logging.mdc[:foo] 62 | assert_equal 'buz', Logging.mdc[:baz] 63 | assert_nil Logging.mdc[:foobar] 64 | assert_equal 2, Logging.mdc.stack.length 65 | 66 | Logging.mdc.pop 67 | assert Logging.mdc.context.empty? 68 | assert_equal 1, Logging.mdc.stack.length 69 | 70 | Logging.mdc.pop 71 | assert Logging.mdc.context.empty? 72 | assert_equal 1, Logging.mdc.stack.length 73 | end 74 | 75 | def test_thread_uniqueness 76 | Logging.mdc['foo'] = 'bar' 77 | Logging.mdc['baz'] = 'buz' 78 | 79 | t = Thread.new { 80 | sleep 81 | 82 | Logging.mdc.clear 83 | assert_nil Logging.mdc['foo'] 84 | assert_nil Logging.mdc['baz'] 85 | 86 | Logging.mdc['foo'] = 42 87 | assert_equal 42, Logging.mdc['foo'] 88 | } 89 | 90 | Thread.pass until t.status == 'sleep' 91 | t.run 92 | t.join 93 | 94 | assert_equal 'bar', Logging.mdc['foo'] 95 | assert_equal 'buz', Logging.mdc['baz'] 96 | end 97 | 98 | def test_thread_inheritance 99 | Logging.mdc['foo'] = 'bar' 100 | Logging.mdc['baz'] = 'buz' 101 | Logging.mdc.push(:foo => 1, 'foobar' => 'something else') 102 | 103 | t = Thread.new(Logging.mdc.context) { |context| 104 | sleep 105 | 106 | assert_not_equal context.object_id, Logging.mdc.context.object_id 107 | 108 | if Logging::INHERIT_CONTEXT 109 | assert_equal 1, Logging.mdc['foo'] 110 | assert_equal 'buz', Logging.mdc['baz'] 111 | assert_equal 'something else', Logging.mdc['foobar'] 112 | assert_nil Logging.mdc['unique'] 113 | 114 | assert_equal 1, Logging.mdc.stack.length 115 | else 116 | assert_nil Logging.mdc['foo'] 117 | assert_nil Logging.mdc['baz'] 118 | assert_nil Logging.mdc['foobar'] 119 | assert_nil Logging.mdc['unique'] 120 | 121 | assert_equal 1, Logging.mdc.stack.length 122 | end 123 | } 124 | 125 | Thread.pass until t.status == 'sleep' 126 | 127 | Logging.mdc.pop 128 | Logging.mdc['unique'] = 'value' 129 | 130 | t.run 131 | t.join 132 | end 133 | 134 | end # class TestMappedDiagnosticContext 135 | end # module TestLogging 136 | -------------------------------------------------------------------------------- /logging.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # stub: logging 2.4.0 ruby lib 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "logging".freeze 6 | s.version = "2.4.0".freeze 7 | 8 | s.license = "MIT" 9 | s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= 10 | s.require_paths = ["lib".freeze] 11 | s.authors = ["Tim Pease".freeze] 12 | s.date = "2024-06-08" 13 | s.description = "**Logging** is a flexible logging library for use in Ruby programs based on the\ndesign of Java's log4j library. It features a hierarchical logging system,\ncustom level names, multiple output destinations per log event, custom\nformatting, and more.".freeze 14 | s.email = "tim.pease@gmail.com".freeze 15 | s.extra_rdoc_files = ["History.txt".freeze] 16 | s.files = ["History.txt".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "examples/appenders.rb".freeze, "examples/classes.rb".freeze, "examples/colorization.rb".freeze, "examples/custom_log_levels.rb".freeze, "examples/fork.rb".freeze, "examples/formatting.rb".freeze, "examples/hierarchies.rb".freeze, "examples/layouts.rb".freeze, "examples/lazy.rb".freeze, "examples/loggers.rb".freeze, "examples/mdc.rb".freeze, "examples/names.rb".freeze, "examples/rails4.rb".freeze, "examples/reusing_layouts.rb".freeze, "examples/rspec_integration.rb".freeze, "examples/simple.rb".freeze, "lib/logging.rb".freeze, "lib/logging/appender.rb".freeze, "lib/logging/appenders.rb".freeze, "lib/logging/appenders/buffering.rb".freeze, "lib/logging/appenders/console.rb".freeze, "lib/logging/appenders/file.rb".freeze, "lib/logging/appenders/io.rb".freeze, "lib/logging/appenders/rolling_file.rb".freeze, "lib/logging/appenders/string_io.rb".freeze, "lib/logging/appenders/syslog.rb".freeze, "lib/logging/color_scheme.rb".freeze, "lib/logging/diagnostic_context.rb".freeze, "lib/logging/filter.rb".freeze, "lib/logging/filters.rb".freeze, "lib/logging/filters/level.rb".freeze, "lib/logging/layout.rb".freeze, "lib/logging/layouts.rb".freeze, "lib/logging/layouts/basic.rb".freeze, "lib/logging/layouts/parseable.rb".freeze, "lib/logging/layouts/pattern.rb".freeze, "lib/logging/log_event.rb".freeze, "lib/logging/logger.rb".freeze, "lib/logging/proxy.rb".freeze, "lib/logging/rails_compat.rb".freeze, "lib/logging/repository.rb".freeze, "lib/logging/root_logger.rb".freeze, "lib/logging/utils.rb".freeze, "lib/logging/version.rb".freeze, "lib/rspec/logging_helper.rb".freeze, "lib/spec/logging_helper.rb".freeze, "logging.gemspec".freeze, "script/bootstrap".freeze, "script/console".freeze, "test/appenders/test_async_flushing.rb".freeze, "test/appenders/test_buffered_io.rb".freeze, "test/appenders/test_console.rb".freeze, "test/appenders/test_file.rb".freeze, "test/appenders/test_io.rb".freeze, "test/appenders/test_rolling_file.rb".freeze, "test/appenders/test_string_io.rb".freeze, "test/appenders/test_syslog.rb".freeze, "test/benchmark.rb".freeze, "test/layouts/test_basic.rb".freeze, "test/layouts/test_color_pattern.rb".freeze, "test/layouts/test_json.rb".freeze, "test/layouts/test_nested_exceptions.rb".freeze, "test/layouts/test_pattern.rb".freeze, "test/layouts/test_yaml.rb".freeze, "test/performance.rb".freeze, "test/setup.rb".freeze, "test/test_appender.rb".freeze, "test/test_color_scheme.rb".freeze, "test/test_filter.rb".freeze, "test/test_layout.rb".freeze, "test/test_log_event.rb".freeze, "test/test_logger.rb".freeze, "test/test_logging.rb".freeze, "test/test_mapped_diagnostic_context.rb".freeze, "test/test_nested_diagnostic_context.rb".freeze, "test/test_proxy.rb".freeze, "test/test_repository.rb".freeze, "test/test_root_logger.rb".freeze, "test/test_utils.rb".freeze] 17 | s.homepage = "http://rubygems.org/gems/logging".freeze 18 | s.licenses = ["MIT".freeze] 19 | s.rdoc_options = ["--main".freeze, "README.md".freeze] 20 | s.rubygems_version = "3.5.3".freeze 21 | s.summary = "A flexible and extendable logging library for Ruby".freeze 22 | 23 | s.specification_version = 4 24 | 25 | s.add_runtime_dependency(%q.freeze, ["~> 1.1".freeze]) 26 | s.add_runtime_dependency(%q.freeze, ["~> 1.14".freeze]) 27 | s.add_development_dependency(%q.freeze, ["~> 3.3".freeze]) 28 | s.add_development_dependency(%q.freeze, ["~> 1.3".freeze]) 29 | s.add_development_dependency(%q.freeze, ["~> 3.9.0".freeze]) 30 | end 31 | -------------------------------------------------------------------------------- /test/appenders/test_io.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('../setup', File.dirname(__FILE__)) 3 | 4 | module TestLogging 5 | module TestAppenders 6 | 7 | class TestIO < Test::Unit::TestCase 8 | include LoggingTestCase 9 | 10 | def setup 11 | super 12 | 13 | @appender = Logging.appenders.string_io('test_appender') 14 | @sio = @appender.sio 15 | @levels = Logging::LEVELS 16 | end 17 | 18 | def test_append 19 | event = Logging::LogEvent.new('TestLogger', @levels['warn'], 20 | [1, 2, 3, 4], false) 21 | @appender.append event 22 | assert_equal " WARN TestLogger : #{[1, 2, 3, 4]}\n", readline 23 | assert_nil(readline) 24 | 25 | event.level = @levels['debug'] 26 | event.data = 'the big log message' 27 | @appender.append event 28 | assert_equal "DEBUG TestLogger : the big log message\n", readline 29 | assert_nil(readline) 30 | 31 | @appender.close 32 | assert_raise(RuntimeError) {@appender.append event} 33 | end 34 | 35 | def test_append_error 36 | # setup an internal logger to capture error messages from the IO 37 | # appender 38 | log = Logging.appenders.string_io('__internal_io') 39 | Logging.logger[Logging].add_appenders(log) 40 | Logging.logger[Logging].level = 'all' 41 | 42 | # close the string IO object so we get an error 43 | @sio.close 44 | event = Logging::LogEvent.new('TestLogger', @levels['warn'], 45 | [1, 2, 3, 4], false) 46 | @appender.append event 47 | 48 | assert_equal "INFO Logging : appender \"test_appender\" has been disabled", log.readline.strip 49 | assert_equal "ERROR Logging : not opened for writing", log.readline.strip 50 | 51 | assert_equal false, @appender.closed? 52 | assert_equal 5, @appender.level 53 | end 54 | 55 | def test_close 56 | assert_equal false, @sio.closed? 57 | assert_equal false, @appender.closed? 58 | 59 | @appender.close 60 | assert_equal true, @sio.closed? 61 | assert_equal true, @appender.closed? 62 | 63 | [STDIN, STDERR, STDOUT].each do |io| 64 | @appender = Logging.appenders.io('test', io) 65 | @appender.close 66 | assert_equal false, io.closed? 67 | assert_equal true, @appender.closed? 68 | end 69 | end 70 | 71 | def test_concat 72 | @appender << "this is a test message\n" 73 | assert_equal "this is a test message\n", readline 74 | assert_nil(readline) 75 | 76 | @appender << "this is another message\n" 77 | @appender << "some other line\n" 78 | assert_equal "this is another message\n", readline 79 | assert_equal "some other line\n", readline 80 | assert_nil(readline) 81 | 82 | @appender.close 83 | assert_raise(RuntimeError) {@appender << 'message'} 84 | end 85 | 86 | def test_concat_error 87 | # setup an internal logger to capture error messages from the IO 88 | # appender 89 | log = Logging.appenders.string_io('__internal_io') 90 | Logging.logger[Logging].add_appenders(log) 91 | Logging.logger[Logging].level = 'all' 92 | 93 | # close the string IO object so we get an error 94 | @sio.close 95 | @appender << 'oopsy' 96 | 97 | assert_equal "INFO Logging : appender \"test_appender\" has been disabled", log.readline.strip 98 | assert_equal "ERROR Logging : not opened for writing", log.readline.strip 99 | 100 | # and the appender does not close itself 101 | assert_equal false, @appender.closed? 102 | assert_equal 5, @appender.level 103 | end 104 | 105 | def test_flush 106 | @appender.buffer << 'flush' 107 | assert_nil @appender.readline 108 | 109 | @appender.flush 110 | assert_equal 'flush', @appender.readline 111 | end 112 | 113 | def test_initialize 114 | assert_raise(EOFError) {@sio.readline} 115 | assert_raise(TypeError) {Logging.appenders.io('test', [])} 116 | end 117 | 118 | private 119 | def readline 120 | @appender.readline 121 | end 122 | 123 | end # class TestIO 124 | 125 | end # module TestAppenders 126 | end # module TestLogging 127 | 128 | -------------------------------------------------------------------------------- /test/test_repository.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('setup', File.dirname(__FILE__)) 3 | 4 | module TestLogging 5 | 6 | class TestRepository < Test::Unit::TestCase 7 | include LoggingTestCase 8 | 9 | def setup 10 | super 11 | @repo = ::Logging::Repository.instance 12 | end 13 | 14 | def test_instance 15 | assert_same @repo, ::Logging::Repository.instance 16 | end 17 | 18 | def test_aref 19 | root = @repo[:root] 20 | assert_same root, @repo[:root] 21 | 22 | a = [] 23 | ::Logging::Logger.new a 24 | assert_same @repo['Array'], @repo[Array] 25 | assert_same @repo['Array'], @repo[a] 26 | 27 | assert_not_same @repo['Array'], @repo[:root] 28 | 29 | ::Logging::Logger.new 'A' 30 | ::Logging::Logger.new 'A::B' 31 | assert_not_same @repo['A'], @repo['A::B'] 32 | end 33 | 34 | def test_aset 35 | root = @repo[:root] 36 | @repo[:root] = 'root' 37 | assert_not_same root, @repo[:root] 38 | 39 | assert_nil @repo['blah'] 40 | @repo['blah'] = 'root' 41 | assert_equal 'root', @repo['blah'] 42 | end 43 | 44 | def test_fetch 45 | assert @repo.has_logger?(:root) 46 | assert_same @repo[:root], @repo.fetch(:root) 47 | 48 | assert !@repo.has_logger?('A') 49 | assert_raise(KeyError) {@repo.fetch 'A'} 50 | 51 | %w(A A::B A::B::C::D A::B::C::E A::B::C::F).each do |name| 52 | ::Logging::Logger.new(name) 53 | end 54 | 55 | assert @repo.has_logger?('A') 56 | assert @repo.has_logger?('A::B') 57 | end 58 | 59 | def test_delete 60 | %w(A A::B A::C A::B::D).each do |name| 61 | ::Logging::Logger.new(name) 62 | end 63 | 64 | assert @repo.has_logger?('A') 65 | assert @repo.has_logger?('A::B') 66 | assert @repo.has_logger?('A::C') 67 | assert @repo.has_logger?('A::B::D') 68 | 69 | assert_raise(RuntimeError) {@repo.delete :root} 70 | assert_raise(KeyError) {@repo.delete 'Does::Not::Exist'} 71 | 72 | @repo.delete 'A' 73 | assert !@repo.has_logger?('A') 74 | assert_equal @repo[:root], @repo['A::B'].parent 75 | assert_equal @repo[:root], @repo['A::C'].parent 76 | assert_equal @repo['A::B'], @repo['A::B::D'].parent 77 | 78 | @repo.delete 'A::B' 79 | assert !@repo.has_logger?('A::B') 80 | assert_equal @repo[:root], @repo['A::B::D'].parent 81 | end 82 | 83 | def test_parent 84 | %w(A A::B A::B::C::D A::B::C::E A::B::C::F).each do |name| 85 | ::Logging::Logger.new(name) 86 | end 87 | 88 | assert_same @repo[:root], @repo.parent('A') 89 | assert_same @repo['A'], @repo.parent('A::B') 90 | assert_same @repo['A::B'], @repo.parent('A::B::C') 91 | assert_same @repo['A::B'], @repo.parent('A::B::C::D') 92 | assert_same @repo['A::B'], @repo.parent('A::B::C::E') 93 | assert_same @repo['A::B'], @repo.parent('A::B::C::F') 94 | 95 | ::Logging::Logger.new('A::B::C') 96 | 97 | assert_same @repo['A::B'], @repo.parent('A::B::C') 98 | assert_same @repo['A::B::C'], @repo.parent('A::B::C::D') 99 | assert_same @repo['A::B::C'], @repo.parent('A::B::C::E') 100 | assert_same @repo['A::B::C'], @repo.parent('A::B::C::F') 101 | 102 | ::Logging::Logger.new('A::B::C::E::G') 103 | 104 | assert_same @repo['A::B::C::E'], @repo.parent('A::B::C::E::G') 105 | 106 | assert_nil @repo.parent('root') 107 | end 108 | 109 | def test_children 110 | ::Logging::Logger.new('A') 111 | 112 | assert_equal [], @repo.children('A') 113 | 114 | ::Logging::Logger.new('A::B') 115 | a = %w(D E F).map {|name| ::Logging::Logger.new('A::B::C::'+name)}.sort 116 | 117 | assert_equal [@repo['A::B']], @repo.children('A') 118 | assert_equal a, @repo.children('A::B') 119 | assert_equal [], @repo.children('A::B::C') 120 | 121 | ::Logging::Logger.new('A::B::C') 122 | 123 | assert_equal [@repo['A::B::C']], @repo.children('A::B') 124 | assert_equal a, @repo.children('A::B::C') 125 | 126 | ::Logging::Logger.new('A::B::C::E::G') 127 | 128 | assert_equal a, @repo.children('A::B::C') 129 | assert_equal [@repo['A::B::C::E::G']], @repo.children('A::B::C::E') 130 | 131 | assert_equal [@repo['A'], @repo['Logging']], @repo.children('root') 132 | end 133 | 134 | def test_to_key 135 | assert_equal :root, @repo.to_key(:root) 136 | assert_equal 'Object', @repo.to_key('Object') 137 | assert_equal 'Object', @repo.to_key(Object) 138 | assert_equal 'Object', @repo.to_key(Object.new) 139 | 140 | assert_equal 'String', @repo.to_key(String) 141 | assert_equal 'Array', @repo.to_key([]) 142 | 143 | assert_equal 'blah', @repo.to_key('blah') 144 | assert_equal 'blah', @repo.to_key(:blah) 145 | end 146 | 147 | end # class TestRepository 148 | end # module TestLogging 149 | 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![.github/workflows/ruby.yml](https://github.com/TwP/logging/actions/workflows/ruby.yml/badge.svg)](https://github.com/TwP/logging/actions/workflows/ruby.yml) 2 | 3 | ## Logging 4 | by Tim Pease 5 | 6 | * [Homepage](http://rubygems.org/gems/logging) 7 | * [Github Project](https://github.com/TwP/logging) 8 | 9 | ### Description 10 | 11 | **Logging** is a flexible logging library for use in Ruby programs based on the 12 | design of Java's log4j library. It features a hierarchical logging system, 13 | custom level names, multiple output destinations per log event, custom 14 | formatting, and more. 15 | 16 | ### Installation 17 | 18 | ``` 19 | gem install logging 20 | ``` 21 | 22 | ### Examples 23 | 24 | This example configures a logger to output messages in a format similar to the 25 | core ruby Logger class. Only log messages that are warnings or higher will be 26 | logged. 27 | 28 | ```ruby 29 | require 'logging' 30 | 31 | logger = Logging.logger(STDOUT) 32 | logger.level = :warn 33 | 34 | logger.debug "this debug message will not be output by the logger" 35 | logger.warn "this is your last warning" 36 | ``` 37 | 38 | In this example, a single logger is created that will append to STDOUT and to a 39 | file. Only log messages that are informational or higher will be logged. 40 | 41 | ```ruby 42 | require 'logging' 43 | 44 | logger = Logging.logger['example_logger'] 45 | logger.level = :info 46 | 47 | logger.add_appenders \ 48 | Logging.appenders.stdout, 49 | Logging.appenders.file('example.log') 50 | 51 | logger.debug "this debug message will not be output by the logger" 52 | logger.info "just some friendly advice" 53 | ``` 54 | 55 | The Logging library was created to allow each class in a program to have its 56 | own configurable logger. The logging level for a particular class can be 57 | changed independently of all other loggers in the system. This example shows 58 | the recommended way of accomplishing this. 59 | 60 | ```ruby 61 | require 'logging' 62 | 63 | Logging.logger['FirstClass'].level = :warn 64 | Logging.logger['SecondClass'].level = :debug 65 | 66 | class FirstClass 67 | def initialize 68 | @logger = Logging.logger[self] 69 | end 70 | 71 | def some_method 72 | @logger.debug "some method was called on #{self.inspect}" 73 | end 74 | end 75 | 76 | class SecondClass 77 | def initialize 78 | @logger = Logging.logger[self] 79 | end 80 | 81 | def another_method 82 | @logger.debug "another method was called on #{self.inspect}" 83 | end 84 | end 85 | ``` 86 | 87 | There are many more examples in the [examples folder](/examples) of the logging 88 | package. The recommended reading order is the following: 89 | 90 | * [simple.rb](/examples/simple.rb) 91 | * [rspec_integration.rb](/examples/rspec_integration.rb) 92 | * [loggers.rb](/examples/loggers.rb) 93 | * [classes.rb](/examples/classes.rb) 94 | * [hierarchies.rb](/examples/hierarchies.rb) 95 | * [names.rb](/examples/names.rb) 96 | * [lazy.rb](/examples/lazy.rb) 97 | * [appenders.rb](/examples/appenders.rb) 98 | * [layouts.rb](/examples/layouts.rb) 99 | * [reusing_layouts.rb](/examples/reusing_layouts.rb) 100 | * [formatting.rb](/examples/formatting.rb) 101 | * [colorization.rb](/examples/colorization.rb) 102 | * [fork.rb](/examples/fork.rb) 103 | * [mdc.rb](/examples/mdc.rb) 104 | 105 | ### Extending 106 | 107 | The Logging framework is extensible via the [little-plugger](https://github.com/twp/little-plugger) 108 | gem-based plugin system. New appenders, layouts, or filters can be released as ruby 109 | gems. When installed locally, the Logging framework will automatically detect 110 | these gems as plugins and make them available for use. 111 | 112 | The [logging-email](https://github.com/twp/logging-email) plugin is a good 113 | example to follow. It includes a [`lib/logging/plugins/email.rb`](https://github.com/twp/logging-email/tree/master/lib/logging/plugins/email.rb) 114 | file which is detected by the plugin framework. This file declares a 115 | `Logging::Plugins::Email.initialize_email` method that is called when the plugin 116 | is loaded. 117 | 118 | The three steps for creating a plugin are: 119 | 120 | * create a new Ruby gem: `logging-` 121 | * include a plugin file: `lib/logging/plugins/.rb` 122 | * define a plugin initializer: `Logging::Plugins::.initialize_` 123 | 124 | ### Development 125 | 126 | The Logging source code relies on the Mr Bones project for default rake tasks. 127 | You will need to install the Mr Bones gem if you want to build or test the 128 | logging gem. Conveniently there is a bootstrap script that you can run to setup 129 | your development environment. 130 | 131 | ``` 132 | script/bootstrap 133 | ``` 134 | 135 | This will install the Mr Bones gem and the required Ruby gems for development. 136 | After this is done you can rake `rake -T` to see the available rake tasks. 137 | 138 | ### License 139 | 140 | The MIT License - see the [LICENSE](/LICENSE) file for the full text. 141 | -------------------------------------------------------------------------------- /test/appenders/test_file.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path('../setup', File.dirname(__FILE__)) 4 | 5 | module TestLogging 6 | module TestAppenders 7 | 8 | class TestFile < Test::Unit::TestCase 9 | include LoggingTestCase 10 | 11 | NAME = 'logfile' 12 | 13 | def setup 14 | super 15 | Logging.init 16 | 17 | FileUtils.mkdir [File.join(@tmpdir, 'dir'), File.join(@tmpdir, 'uw_dir')] 18 | FileUtils.chmod 0555, File.join(@tmpdir, 'uw_dir') 19 | FileUtils.touch File.join(@tmpdir, 'uw_file') 20 | FileUtils.chmod 0444, File.join(@tmpdir, 'uw_file') 21 | end 22 | 23 | def test_factory_method_validates_input 24 | assert_raise(ArgumentError) do 25 | Logging.appenders.file 26 | end 27 | end 28 | 29 | def test_class_assert_valid_logfile 30 | log = File.join(@tmpdir, 'uw_dir', 'file.log') 31 | assert_raise(ArgumentError) do 32 | Logging.appenders.file(log).class.assert_valid_logfile(log) 33 | end 34 | 35 | log = File.join(@tmpdir, 'dir') 36 | assert_raise(ArgumentError) do 37 | Logging.appenders.file(log).class.assert_valid_logfile(log) 38 | end 39 | 40 | log = File.join(@tmpdir, 'uw_file') 41 | assert_raise(ArgumentError) do 42 | Logging.appenders.file(log).class.assert_valid_logfile(log) 43 | end 44 | 45 | log = File.join(@tmpdir, 'file.log') 46 | assert Logging.appenders.file(log).class.assert_valid_logfile(log) 47 | end 48 | 49 | def test_initialize 50 | log = File.join(@tmpdir, 'file.log') 51 | appender = Logging.appenders.file(NAME, :filename => log) 52 | assert_equal 'logfile', appender.name 53 | assert_equal ::File.expand_path(log), appender.filename 54 | appender << "This will be the first line\n" 55 | appender << "This will be the second line\n" 56 | appender.flush 57 | File.open(log, 'r') do |file| 58 | assert_equal "This will be the first line\n", file.readline 59 | assert_equal "This will be the second line\n", file.readline 60 | assert_raise(EOFError) {file.readline} 61 | end 62 | cleanup 63 | 64 | appender = Logging.appenders.file(NAME, :filename => log) 65 | assert_equal 'logfile', appender.name 66 | assert_equal ::File.expand_path(log), appender.filename 67 | appender << "This will be the third line\n" 68 | appender.flush 69 | File.open(log, 'r') do |file| 70 | assert_equal "This will be the first line\n", file.readline 71 | assert_equal "This will be the second line\n", file.readline 72 | assert_equal "This will be the third line\n", file.readline 73 | assert_raise(EOFError) {file.readline} 74 | end 75 | cleanup 76 | 77 | appender = Logging.appenders.file(NAME, :filename => log, 78 | :truncate => true) 79 | assert_equal 'logfile', appender.name 80 | appender << "The file was truncated\n" 81 | appender.flush 82 | File.open(log, 'r') do |file| 83 | assert_equal "The file was truncated\n", file.readline 84 | assert_raise(EOFError) {file.readline} 85 | end 86 | cleanup 87 | end 88 | 89 | def test_changing_directories 90 | log = File.join(@tmpdir, 'file.log') 91 | appender = Logging.appenders.file(NAME, :filename => log) 92 | 93 | assert_equal 'logfile', appender.name 94 | assert_equal ::File.expand_path(log), appender.filename 95 | 96 | begin 97 | pwd = Dir.pwd 98 | Dir.chdir @tmpdir 99 | assert_nothing_raised { appender.reopen } 100 | ensure 101 | Dir.chdir pwd 102 | end 103 | end 104 | 105 | def test_encoding 106 | log = File.join(@tmpdir, 'file-encoding.log') 107 | appender = Logging.appenders.file(NAME, :filename => log, :encoding => 'ASCII') 108 | 109 | appender << "A normal line of text\n" 110 | appender << "ümlaut\n" 111 | appender.close 112 | 113 | lines = File.readlines(log, :encoding => 'UTF-8') 114 | assert_equal "A normal line of text\n", lines[0] 115 | assert_equal "ümlaut\n", lines[1] 116 | 117 | cleanup 118 | end 119 | 120 | def test_reopening_should_not_truncate_the_file 121 | log = File.join(@tmpdir, 'truncate.log') 122 | appender = Logging.appenders.file(NAME, filename: log, truncate: true) 123 | 124 | appender << "This will be the first line\n" 125 | appender << "This will be the second line\n" 126 | appender << "This will be the third line\n" 127 | appender.reopen 128 | 129 | File.open(log, 'r') do |file| 130 | assert_equal "This will be the first line\n", file.readline 131 | assert_equal "This will be the second line\n", file.readline 132 | assert_equal "This will be the third line\n", file.readline 133 | assert_raise(EOFError) {file.readline} 134 | end 135 | 136 | cleanup 137 | end 138 | 139 | private 140 | def cleanup 141 | unless Logging.appenders[NAME].nil? 142 | Logging.appenders[NAME].close false 143 | Logging.appenders[NAME] = nil 144 | end 145 | end 146 | end # TestFile 147 | 148 | end # TestAppenders 149 | end # TestLogging 150 | 151 | -------------------------------------------------------------------------------- /lib/logging/utils.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'thread' 3 | require 'rbconfig' 4 | 5 | # -------------------------------------------------------------------------- 6 | class String 7 | 8 | # call-seq: 9 | # shrink( width, ellipses = '...' ) #=> string 10 | # 11 | # Shrink the size of the current string to the given _width_ by removing 12 | # characters from the middle of the string and replacing them with 13 | # _ellipses_. If the _width_ is greater than the length of the string, the 14 | # string is returned unchanged. If the _width_ is less than the length of 15 | # the _ellipses_, then the _ellipses_ are returned. 16 | # 17 | def shrink( width, ellipses = '...') 18 | raise ArgumentError, "width cannot be negative: #{width}" if width < 0 19 | 20 | return self if length <= width 21 | 22 | remove = length - width + ellipses.length 23 | return ellipses.dup if remove >= length 24 | 25 | left_end = (length + 1 - remove) / 2 26 | right_start = left_end + remove 27 | 28 | left = self[0,left_end] 29 | right = self[right_start,length-right_start] 30 | 31 | left << ellipses << right 32 | end 33 | end 34 | 35 | # -------------------------------------------------------------------------- 36 | class Module 37 | 38 | # call-seq: 39 | # logger_name #=> string 40 | # 41 | # Returns a predictable logger name for the current module or class. If 42 | # used within an anonymous class, the first non-anonymous class name will 43 | # be used as the logger name. If used within a meta-class, the name of the 44 | # actual class will be used as the logger name. If used within an 45 | # anonymous module, the string 'anonymous' will be returned. 46 | # 47 | def logger_name 48 | return name unless name.nil? or name.empty? 49 | 50 | # check if this is a metaclass (or eigenclass) 51 | if ancestors.include? Class 52 | inspect =~ %r/#]+)>/ 53 | return $1 54 | end 55 | 56 | # see if we have a superclass 57 | if respond_to? :superclass 58 | return superclass.logger_name 59 | end 60 | 61 | # we are an anonymous module 62 | ::Logging.log_internal(-2) { 63 | 'cannot return a predictable, unique name for anonymous modules' 64 | } 65 | return 'anonymous' 66 | end 67 | end 68 | 69 | # -------------------------------------------------------------------------- 70 | class File 71 | 72 | # Returns true if another process holds an exclusive lock on the 73 | # file. Returns false if this is not the case. 74 | # 75 | # If a block of code is passed to this method, it will be run iff 76 | # this process can obtain an exclusive lock on the file. The block will be 77 | # run while this lock is held, and the exclusive lock will be released when 78 | # the method returns. 79 | # 80 | # The exclusive lock is requested in a non-blocking mode. This method will 81 | # return immediately (and the block will not be executed) if an exclusive 82 | # lock cannot be obtained. 83 | # 84 | def flock? 85 | status = flock(LOCK_EX|LOCK_NB) 86 | case status 87 | when false; true 88 | when 0; block_given? ? yield : false 89 | else 90 | raise SystemCallError, "flock failed with status: #{status}" 91 | end 92 | ensure 93 | flock LOCK_UN 94 | end 95 | 96 | # Execute a block in the context of a shared lock on this file. A 97 | # shared lock will be obtained on the file, the block executed, and the lock 98 | # released. 99 | # 100 | def flock_sh 101 | flock LOCK_SH 102 | yield 103 | ensure 104 | flock LOCK_UN 105 | end 106 | 107 | # :stopdoc: 108 | conf = defined?(RbConfig) ? RbConfig::CONFIG : Config::CONFIG 109 | if conf['host_os'] =~ /mswin|windows|cygwin|mingw/i 110 | # don't lock files on windows 111 | undef :flock?, :flock_sh 112 | def flock?() yield; end 113 | def flock_sh() yield; end 114 | end 115 | # :startdoc: 116 | 117 | end 118 | 119 | # -------------------------------------------------------------------------- 120 | module FileUtils 121 | 122 | # Concatenate the contents of the _src_ file to the end of the _dest_ file. 123 | # If the _dest_ file does not exist, then the _src_ file is copied to the 124 | # _dest_ file using +copy_file+. 125 | # 126 | def concat( src, dest ) 127 | if File.exist?(dest) 128 | bufsize = File.stat(dest).blksize || 8192 129 | buffer = String.new 130 | 131 | File.open(dest, 'a') { |d| 132 | File.open(src, 'r') { |r| 133 | while bytes = r.read(bufsize, buffer) 134 | d.syswrite bytes 135 | end 136 | } 137 | } 138 | else 139 | copy_file(src, dest) 140 | end 141 | end 142 | module_function :concat 143 | end 144 | 145 | # -------------------------------------------------------------------------- 146 | class ReentrantMutex < Mutex 147 | 148 | def initialize 149 | super 150 | @locker = nil 151 | end 152 | 153 | alias_method :original_synchronize, :synchronize 154 | 155 | def synchronize 156 | if @locker == Thread.current 157 | yield 158 | else 159 | original_synchronize { 160 | begin 161 | @locker = Thread.current 162 | yield 163 | ensure 164 | @locker = nil 165 | end 166 | } 167 | end 168 | end 169 | end # ReentrantMutex 170 | 171 | -------------------------------------------------------------------------------- /test/test_layout.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('setup', File.dirname(__FILE__)) 3 | 4 | module TestLogging 5 | 6 | class TestLayout < Test::Unit::TestCase 7 | include LoggingTestCase 8 | 9 | def setup 10 | super 11 | @layout = ::Logging::Layout.new 12 | end 13 | 14 | def test_header 15 | assert_equal '', @layout.header 16 | end 17 | 18 | def test_initialize 19 | obj_format = lambda {|l| l.instance_variable_get :@obj_format} 20 | 21 | assert_equal :string, obj_format[@layout] 22 | 23 | @layout = ::Logging::Layout.new :format_as => 'blah' 24 | assert_equal :string, obj_format[@layout] 25 | 26 | @layout = ::Logging::Layout.new :format_as => :inspect 27 | assert_equal :inspect, obj_format[@layout] 28 | 29 | @layout = ::Logging::Layout.new :format_as => :json 30 | assert_equal :json, obj_format[@layout] 31 | 32 | @layout = ::Logging::Layout.new :format_as => :yaml 33 | assert_equal :yaml, obj_format[@layout] 34 | 35 | @layout = ::Logging::Layout.new 36 | assert_equal :string, obj_format[@layout] 37 | 38 | ::Logging.format_as :yaml 39 | @layout = ::Logging::Layout.new 40 | assert_equal :yaml, obj_format[@layout] 41 | end 42 | 43 | def test_footer 44 | assert_equal '', @layout.footer 45 | end 46 | 47 | def test_format 48 | assert_nil @layout.format(::Logging::LogEvent.new('a','b','c',false)) 49 | end 50 | 51 | def test_format_obj 52 | obj = 'test string' 53 | r = @layout.format_obj obj 54 | assert_same obj, r 55 | 56 | obj = RuntimeError.new 57 | r = @layout.format_obj obj 58 | assert_equal ' RuntimeError', r 59 | 60 | obj = TypeError.new 'only works with Integers' 61 | r = @layout.format_obj obj 62 | assert_equal ' only works with Integers', r 63 | 64 | obj = Exception.new 'some exception' 65 | obj.set_backtrace %w( this is the backtrace ) 66 | r = @layout.format_obj obj 67 | obj = " some exception\n\tthis\n\tis\n\tthe\n\tbacktrace" 68 | assert_equal obj, r 69 | 70 | obj = [1, 2, 3, 4] 71 | r = @layout.format_obj obj 72 | assert_equal " #{[1,2,3,4]}", r 73 | 74 | obj = %w( one two three four ) 75 | @layout = ::Logging::Layout.new :format_as => :inspect 76 | r = @layout.format_obj obj 77 | assert_equal ' ["one", "two", "three", "four"]', r 78 | 79 | @layout = ::Logging::Layout.new :format_as => :json 80 | r = @layout.format_obj obj 81 | assert_equal ' ["one","two","three","four"]', r 82 | 83 | @layout = ::Logging::Layout.new :format_as => :yaml 84 | r = @layout.format_obj obj 85 | assert_match %r/\A \n--- ?\n- one\n- two\n- three\n- four\n/, r 86 | 87 | r = @layout.format_obj Class 88 | assert_match %r/\A (\n--- !ruby\/class ')?Class('\n)?/, r 89 | end 90 | 91 | def test_format_obj_without_backtrace 92 | @layout = ::Logging::Layout.new :backtrace => 'off' 93 | 94 | obj = Exception.new 'some exception' 95 | obj.set_backtrace %w( this is the backtrace ) 96 | r = @layout.format_obj obj 97 | obj = " some exception" 98 | assert_equal obj, r 99 | 100 | ::Logging.backtrace :off 101 | @layout = ::Logging::Layout.new 102 | 103 | obj = ArgumentError.new 'wrong type of argument' 104 | obj.set_backtrace %w( this is the backtrace ) 105 | r = @layout.format_obj obj 106 | obj = " wrong type of argument" 107 | assert_equal obj, r 108 | end 109 | 110 | def test_initializer 111 | assert_raise(ArgumentError) {::Logging::Layout.new :backtrace => 'foo'} 112 | end 113 | 114 | def test_backtrace_accessors 115 | assert @layout.backtrace? 116 | 117 | @layout.backtrace = :off 118 | refute @layout.backtrace? 119 | 120 | @layout.backtrace = 'on' 121 | assert_equal true, @layout.backtrace 122 | end 123 | 124 | def test_utc_offset 125 | assert_nil @layout.utc_offset 126 | 127 | @layout.utc_offset = 0 128 | assert_equal 0, @layout.utc_offset 129 | 130 | @layout.utc_offset = "UTC" 131 | assert_equal 0, @layout.utc_offset 132 | 133 | @layout.utc_offset = "+01:00" 134 | assert_equal "+01:00", @layout.utc_offset 135 | 136 | assert_raise(ArgumentError) {@layout.utc_offset = "06:00"} 137 | 138 | @layout.utc_offset = nil 139 | ::Logging.utc_offset = "UTC" 140 | assert_nil @layout.utc_offset 141 | 142 | layout = ::Logging::Layout.new 143 | assert_equal 0, layout.utc_offset 144 | end 145 | 146 | def test_apply_utc_offset 147 | time = Time.now.freeze 148 | 149 | offset_time = @layout.apply_utc_offset(time) 150 | assert_same time, offset_time 151 | 152 | @layout.utc_offset = "UTC" 153 | offset_time = @layout.apply_utc_offset(time) 154 | assert_not_same time, offset_time 155 | assert offset_time.utc? 156 | 157 | @layout.utc_offset = "+01:00" 158 | offset_time = @layout.apply_utc_offset(time) 159 | assert_not_same time, offset_time 160 | assert !offset_time.utc? 161 | assert_equal 3600, offset_time.utc_offset 162 | end 163 | end # class TestLayout 164 | end # module TestLogging 165 | 166 | -------------------------------------------------------------------------------- /test/appenders/test_syslog.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('../setup', File.dirname(__FILE__)) 3 | 4 | if HAVE_SYSLOG 5 | 6 | module TestLogging 7 | module TestAppenders 8 | 9 | class TestSyslog < Test::Unit::TestCase 10 | include LoggingTestCase 11 | include ::Syslog::Constants 12 | 13 | def setup 14 | super 15 | Logging.init 16 | @levels = Logging::LEVELS 17 | @logopt = 0 18 | @logopt |= ::Syslog::LOG_NDELAY if defined?(::Syslog::LOG_NDELAY) 19 | @logopt |= ::Syslog::LOG_PERROR if defined?(::Syslog::LOG_PERROR) 20 | end 21 | 22 | def test_factory_method_validates_input 23 | assert_raise(ArgumentError) do 24 | Logging.appenders.syslog 25 | end 26 | end 27 | 28 | def test_append 29 | return if RUBY_PLATFORM =~ %r/cygwin|java/i 30 | 31 | stderr = IO::pipe 32 | 33 | pid = fork do 34 | stderr[0].close 35 | STDERR.reopen(stderr[1]) 36 | stderr[1].close 37 | 38 | appender = create_syslog 39 | event = Logging::LogEvent.new('TestLogger', @levels['info'], 40 | [1, 2, 3, 4], false) 41 | appender.append event 42 | event.level = @levels['debug'] 43 | event.data = 'the big log message' 44 | appender.append event 45 | 46 | appender.level = :warn 47 | event.level = @levels['info'] 48 | event.data = 'this message should not get logged' 49 | appender.append event 50 | event.level = @levels['warn'] 51 | event.data = 'this is your last warning' 52 | appender.append event 53 | 54 | exit! 55 | end 56 | 57 | stderr[1].close 58 | Process.waitpid(pid) 59 | 60 | if defined?(::Syslog::LOG_PERROR) 61 | assert_match(%r/INFO TestLogger : #{Regexp.escape [1,2,3,4].to_s}/, stderr[0].gets) 62 | assert_match(%r/DEBUG TestLogger : the big log message/, stderr[0].gets) 63 | assert_match(%r/WARN TestLogger : this is your last warning/, stderr[0].gets) 64 | end 65 | end 66 | 67 | def test_append_error 68 | appender = create_syslog 69 | appender.close false 70 | 71 | event = Logging::LogEvent.new('TestLogger', @levels['warn'], 72 | [1, 2, 3, 4], false) 73 | assert_raise(RuntimeError) {appender.append event} 74 | assert_equal true, appender.closed? 75 | end 76 | 77 | def test_close 78 | appender = create_syslog 79 | assert_equal false, appender.closed? 80 | 81 | appender.close false 82 | assert_equal true, appender.closed? 83 | end 84 | 85 | def test_concat 86 | return if RUBY_PLATFORM =~ %r/cygwin|java/i 87 | 88 | stderr = IO::pipe 89 | 90 | pid = fork do 91 | stderr[0].close 92 | STDERR.reopen(stderr[1]) 93 | stderr[1].close 94 | 95 | appender = create_syslog 96 | appender << 'this is a test message' 97 | appender << 'this is another message' 98 | appender << 'some other line' 99 | 100 | exit! 101 | end 102 | 103 | stderr[1].close 104 | Process.waitpid(pid) 105 | 106 | if defined?(::Syslog::LOG_PERROR) 107 | assert_match(%r/this is a test message/, stderr[0].gets) 108 | assert_match(%r/this is another message/, stderr[0].gets) 109 | assert_match(%r/some other line/, stderr[0].gets) 110 | end 111 | end 112 | 113 | def test_concat_error 114 | appender = create_syslog 115 | appender.close false 116 | 117 | assert_raise(RuntimeError) {appender << 'oopsy'} 118 | assert_equal true, appender.closed? 119 | end 120 | 121 | def test_map_eq 122 | appender = create_syslog 123 | 124 | assert_equal( 125 | [LOG_DEBUG, LOG_INFO, LOG_WARNING, LOG_ERR, LOG_CRIT], 126 | get_map_from(appender) 127 | ) 128 | 129 | appender.map = { 130 | :debug => LOG_DEBUG, 131 | :info => 'LOG_NOTICE', 132 | :warn => :LOG_WARNING, 133 | :error => 'log_err', 134 | :fatal => :log_alert 135 | } 136 | 137 | assert_equal( 138 | [LOG_DEBUG, LOG_NOTICE, LOG_WARNING, LOG_ERR, LOG_ALERT], 139 | get_map_from(appender) 140 | ) 141 | end 142 | 143 | def test_map_eq_error 144 | appender = create_syslog 145 | 146 | # Object is not a valid syslog level 147 | assert_raise(ArgumentError) do 148 | appender.map = {:debug => Object} 149 | end 150 | 151 | # there is no syslog level named "info" 152 | # it should be "log_info" 153 | assert_raise(NameError) do 154 | appender.map = {:info => 'lg_info'} 155 | end 156 | end 157 | 158 | def test_initialize_map 159 | appender = Logging.appenders.syslog( 160 | 'syslog_test', 161 | :logopt => @logopt, 162 | :map => { 163 | :debug => :log_debug, 164 | :info => :log_info, 165 | :warn => :log_warning, 166 | :error => :log_err, 167 | :fatal => :log_alert 168 | } 169 | ) 170 | 171 | assert_equal( 172 | [LOG_DEBUG, LOG_INFO, LOG_WARNING, LOG_ERR, LOG_ALERT], 173 | get_map_from(appender) 174 | ) 175 | end 176 | 177 | 178 | private 179 | 180 | def create_syslog 181 | layout = Logging.layouts.pattern(:pattern => '%5l %c : %m') 182 | Logging.appenders.syslog( 183 | 'syslog_test', 184 | :logopt => @logopt, 185 | :facility => ::Syslog::LOG_USER, 186 | :layout => layout 187 | ) 188 | end 189 | 190 | def get_map_from( syslog ) 191 | syslog.instance_variable_get :@map 192 | end 193 | 194 | end # class TestSyslog 195 | 196 | end # module TestAppenders 197 | end # module TestLogging 198 | 199 | end # HAVE_SYSLOG 200 | 201 | -------------------------------------------------------------------------------- /test/test_appender.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('setup', File.dirname(__FILE__)) 3 | 4 | module TestLogging 5 | 6 | class TestAppender < Test::Unit::TestCase 7 | include LoggingTestCase 8 | 9 | def setup 10 | super 11 | 12 | ::Logging.init 13 | @levels = ::Logging::LEVELS 14 | @event = ::Logging::LogEvent.new('logger', @levels['debug'], 15 | 'message', false) 16 | @appender = ::Logging::Appender.new 'test_appender' 17 | end 18 | 19 | def test_append 20 | ary = [] 21 | @appender.instance_variable_set :@ary, ary 22 | def @appender.write( event ) 23 | str = event.instance_of?(::Logging::LogEvent) ? 24 | @layout.format(event) : event.to_s 25 | @ary << str 26 | end 27 | 28 | assert_nothing_raised {@appender.append @event} 29 | assert_equal "DEBUG logger : message\n", ary.pop 30 | 31 | @appender.level = :info 32 | @appender.append @event 33 | assert_nil ary.pop 34 | 35 | @event.level = @levels['info'] 36 | @appender.append @event 37 | assert_equal " INFO logger : message\n", ary.pop 38 | 39 | @appender.close 40 | assert_raise(RuntimeError) {@appender.append @event} 41 | end 42 | 43 | def test_append_with_filter 44 | ary = [] 45 | @appender.instance_variable_set :@ary, ary 46 | def @appender.write(event) 47 | @ary << event 48 | end 49 | @appender.level = :debug 50 | 51 | # Excluded 52 | @appender.filters = ::Logging::Filters::Level.new :info 53 | @appender.append @event 54 | assert_nil ary.pop 55 | 56 | # Allowed 57 | @appender.filters = ::Logging::Filters::Level.new :debug 58 | @appender.append @event 59 | assert_equal @event, ary.pop 60 | 61 | # No filter 62 | @appender.filters = nil 63 | @appender.append @event 64 | assert_equal @event, ary.pop 65 | end 66 | 67 | def test_append_with_modifying_filter 68 | ary = [] 69 | @appender.instance_variable_set :@ary, ary 70 | def @appender.write(event) 71 | @ary << event 72 | end 73 | @appender.level = :debug 74 | @appender.filters = [ 75 | ::Logging::Filters::Level.new(:debug, :info), 76 | RedactFilter.new 77 | ] 78 | 79 | # data will be redacted 80 | @appender.append @event 81 | event = ary.pop 82 | assert_not_same @event, event 83 | assert_equal "REDACTED!", event.data 84 | 85 | # event will be filtered out 86 | @event.level = @levels['warn'] 87 | @appender.append @event 88 | assert_nil ary.pop 89 | end 90 | 91 | def test_close 92 | assert_equal false, @appender.closed? 93 | 94 | @appender.close 95 | assert_equal true, @appender.closed? 96 | end 97 | 98 | def test_closed_eh 99 | assert_equal false, @appender.closed? 100 | 101 | @appender.close 102 | assert_equal true, @appender.closed? 103 | end 104 | 105 | def test_concat 106 | ary = [] 107 | @appender.instance_variable_set :@ary, ary 108 | def @appender.write( event ) 109 | str = event.instance_of?(::Logging::LogEvent) ? 110 | @layout.format(event) : event.to_s 111 | @ary << str 112 | end 113 | 114 | assert_nothing_raised {@appender << 'log message'} 115 | assert_equal 'log message', ary.pop 116 | 117 | @appender.level = :off 118 | @appender << 'another log message' 119 | assert_nil ary.pop 120 | 121 | layout = @appender.layout 122 | def layout.footer() 'this is the footer' end 123 | 124 | @appender.close 125 | assert_raise(RuntimeError) {@appender << 'log message'} 126 | assert_equal 'this is the footer', ary.pop 127 | end 128 | 129 | def test_flush 130 | assert_same @appender, @appender.flush 131 | end 132 | 133 | def test_initialize 134 | assert_raise(TypeError) {::Logging::Appender.new 'test', :layout => []} 135 | 136 | layout = ::Logging::Layouts::Basic.new 137 | @appender = ::Logging::Appender.new 'test', :layout => layout 138 | assert_same layout, @appender.instance_variable_get(:@layout) 139 | end 140 | 141 | def test_layout 142 | assert_instance_of ::Logging::Layouts::Basic, @appender.layout 143 | end 144 | 145 | def test_layout_eq 146 | layout = ::Logging::Layouts::Basic.new 147 | assert_not_equal layout, @appender.layout 148 | 149 | assert_raise(TypeError) {@appender.layout = Object.new} 150 | assert_raise(TypeError) {@appender.layout = 'not a layout'} 151 | 152 | @appender.layout = layout 153 | assert_same layout, @appender.layout 154 | end 155 | 156 | def test_level 157 | assert_equal 0, @appender.level 158 | end 159 | 160 | def test_level_eq 161 | assert_equal 0, @appender.level 162 | 163 | assert_raise(ArgumentError) {@appender.level = -1} 164 | assert_raise(ArgumentError) {@appender.level = 6} 165 | assert_raise(ArgumentError) {@appender.level = Object} 166 | assert_raise(ArgumentError) {@appender.level = 'bob'} 167 | assert_raise(ArgumentError) {@appender.level = :wtf} 168 | 169 | @appender.level = 'INFO' 170 | assert_equal 1, @appender.level 171 | 172 | @appender.level = :warn 173 | assert_equal 2, @appender.level 174 | 175 | @appender.level = 'error' 176 | assert_equal 3, @appender.level 177 | 178 | @appender.level = 4 179 | assert_equal 4, @appender.level 180 | 181 | @appender.level = 'off' 182 | assert_equal 5, @appender.level 183 | 184 | @appender.level = :all 185 | assert_equal 0, @appender.level 186 | end 187 | 188 | def test_name 189 | assert_equal 'test_appender', @appender.name 190 | end 191 | 192 | def test_to_s 193 | assert_equal "", @appender.to_s 194 | end 195 | end # class TestAppender 196 | end # module TestLogging 197 | 198 | class RedactFilter < ::Logging::Filter 199 | def allow( event ) 200 | event = event.dup 201 | event.data = "REDACTED!" 202 | event 203 | end 204 | end 205 | 206 | -------------------------------------------------------------------------------- /test/layouts/test_yaml.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | require File.expand_path('../setup', File.dirname(__FILE__)) 3 | 4 | module TestLogging 5 | module TestLayouts 6 | 7 | class TestYaml < Test::Unit::TestCase 8 | include LoggingTestCase 9 | 10 | def setup 11 | super 12 | @layout = Logging.layouts.yaml({}) 13 | @levels = Logging::LEVELS 14 | @date_fmt = '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}(Z|[+-]\d{2}:\d{2})' 15 | Thread.current[:name] = nil 16 | end 17 | 18 | def test_format 19 | h = { 20 | 'level' => 'INFO', 21 | 'logger' => 'ArrayLogger', 22 | 'message' => 'log message' 23 | } 24 | 25 | event = Logging::LogEvent.new('ArrayLogger', @levels['info'], 26 | 'log message', false) 27 | assert_yaml_match h, @layout.format(event) 28 | 29 | event.data = [1, 2, 3, 4] 30 | h['message'] = [1,2,3,4] 31 | assert_yaml_match h, @layout.format(event) 32 | 33 | event.level = @levels['debug'] 34 | event.data = 'and another message' 35 | h['level'] = 'DEBUG' 36 | h['message'] = 'and another message' 37 | assert_yaml_match h, @layout.format(event) 38 | 39 | event.logger = 'Test' 40 | event.level = @levels['fatal'] 41 | event.data = Exception.new 42 | h['level'] = 'FATAL' 43 | h['logger'] = 'Test' 44 | h['message'] = {:class => 'Exception', :message => 'Exception'} 45 | assert_yaml_match h, @layout.format(event) 46 | end 47 | 48 | def test_items 49 | assert_equal %w[timestamp level logger message], @layout.items 50 | end 51 | 52 | def test_items_eq 53 | event = Logging::LogEvent.new('TestLogger', @levels['info'], 54 | ['log message'], false) 55 | 56 | @layout.items = %w[timestamp] 57 | assert_equal %w[timestamp], @layout.items 58 | assert_match %r/\A--- ?\ntimestamp: ["']#@date_fmt["']\n/, @layout.format(event) 59 | 60 | # 'foo' is not a recognized item 61 | assert_raise(ArgumentError) { 62 | @layout.items = %w[timestamp logger foo] 63 | } 64 | end 65 | 66 | def test_items_all 67 | event = Logging::LogEvent.new('TestLogger', @levels['info'], 68 | 'log message', false) 69 | event.file = 'test_file.rb' 70 | event.line = 123 71 | event.method_name = 'method_name' 72 | 73 | @layout.items = %w[logger] 74 | assert_match %r/\A--- ?\nlogger: TestLogger\n/, @layout.format(event) 75 | 76 | @layout.items = %w[file] 77 | assert_match %r/\A--- ?\nfile: test_file.rb\n/, @layout.format(event) 78 | 79 | @layout.items = %w[level] 80 | assert_match %r/\A--- ?\nlevel: INFO\n/, @layout.format(event) 81 | 82 | @layout.items = %w[line] 83 | assert_match %r/\A--- ?\nline: 123\n/, @layout.format(event) 84 | 85 | @layout.items = %w[message] 86 | assert_match %r/\A--- ?\nmessage: log message\n/, @layout.format(event) 87 | 88 | @layout.items = %w[method] 89 | assert_match %r/\A--- ?\nmethod: method_name\n/, @layout.format(event) 90 | 91 | @layout.items = %w[hostname] 92 | assert_match %r/\A--- ?\nhostname: #{Socket.gethostname}\n/, @layout.format(event) 93 | 94 | @layout.items = %w[pid] 95 | assert_match %r/\A--- ?\npid: \d+\n\z/, @layout.format(event) 96 | 97 | @layout.items = %w[millis] 98 | assert_match %r/\A--- ?\nmillis: \d+\n\z/, @layout.format(event) 99 | 100 | @layout.items = %w[thread_id] 101 | assert_match %r/\A--- ?\nthread_id: -?\d+\n\z/, @layout.format(event) 102 | 103 | @layout.items = %w[thread] 104 | assert_match %r/\A--- ?\nthread: ?\n/, @layout.format(event) 105 | Thread.current[:name] = "Main" 106 | assert_match %r/\A--- ?\nthread: Main\n/, @layout.format(event) 107 | 108 | @layout.items = %w[mdc] 109 | assert_match %r/\A--- ?\nmdc: \{\}\n/, @layout.format(event) 110 | 111 | @layout.items = %w[ndc] 112 | assert_match %r/\A--- ?\nndc: \[\]\n/, @layout.format(event) 113 | end 114 | 115 | def test_mdc_output 116 | event = Logging::LogEvent.new('TestLogger', @levels['info'], 117 | 'log message', false) 118 | Logging.mdc['X-Session'] = '123abc' 119 | Logging.mdc['Cookie'] = 'monster' 120 | 121 | @layout.items = %w[timestamp level logger message mdc] 122 | 123 | format = @layout.format(event) 124 | assert_match %r/\nmdc: ?(?:\n (?:X-Session: 123abc|Cookie: monster)\n?){2}/, format 125 | 126 | Logging.mdc.delete 'Cookie' 127 | format = @layout.format(event) 128 | assert_match %r/\nmdc: ?\n X-Session: 123abc\n/, format 129 | end 130 | 131 | def test_ndc_output 132 | event = Logging::LogEvent.new('TestLogger', @levels['info'], 133 | 'log message', false) 134 | Logging.ndc << 'context a' 135 | Logging.ndc << 'context b' 136 | 137 | @layout.items = %w[timestamp level logger message ndc] 138 | 139 | format = @layout.format(event) 140 | assert_match %r/\nndc: ?\n\s*- context a\n\s*- context b\n/, format 141 | 142 | Logging.ndc.pop 143 | format = @layout.format(event) 144 | assert_match %r/\nndc: ?\n\s*- context a\n/, format 145 | 146 | Logging.ndc.pop 147 | format = @layout.format(event) 148 | assert_match %r/\nndc: \[\]\n/, format 149 | end 150 | 151 | def test_utc_offset 152 | layout = Logging.layouts.yaml(:items => %w[timestamp]) 153 | event = Logging::LogEvent.new('TimestampLogger', @levels['info'], 'log message', false) 154 | event.time = Time.utc(2016, 12, 1, 12, 0, 0).freeze 155 | 156 | assert_equal %Q{---\ntimestamp: '2016-12-01T12:00:00.000000Z'\n}, layout.format(event) 157 | 158 | layout.utc_offset = "-06:00" 159 | assert_equal %Q{---\ntimestamp: '2016-12-01T06:00:00.000000-06:00'\n}, layout.format(event) 160 | 161 | layout.utc_offset = "+01:00" 162 | assert_equal %Q{---\ntimestamp: '2016-12-01T13:00:00.000000+01:00'\n}, layout.format(event) 163 | end 164 | 165 | private 166 | 167 | def assert_yaml_match( expected, actual ) 168 | actual = YAML.load(actual) 169 | 170 | assert_instance_of String, actual['timestamp'] 171 | assert_instance_of Time, Time.parse(actual['timestamp']) 172 | assert_equal expected['level'], actual['level'] 173 | assert_equal expected['logger'], actual['logger'] 174 | assert_equal expected['message'], actual['message'] 175 | end 176 | 177 | end # class TestYaml 178 | end # module TestLayouts 179 | end # module TestLogging 180 | 181 | -------------------------------------------------------------------------------- /test/appenders/test_async_flushing.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('../setup', File.dirname(__FILE__)) 3 | 4 | module TestLogging 5 | module TestAppenders 6 | 7 | class TestAsyncFlushing < Test::Unit::TestCase 8 | include LoggingTestCase 9 | 10 | def setup 11 | super 12 | @appender = Logging.appenders.string_io \ 13 | 'test_appender', 14 | :flush_period => 2 15 | 16 | @appender.clear 17 | @sio = @appender.sio 18 | @levels = Logging::LEVELS 19 | begin readline rescue EOFError end 20 | Thread.pass # give the flusher thread a moment to start 21 | end 22 | 23 | def teardown 24 | @appender.close 25 | @appender = nil 26 | super 27 | end 28 | 29 | def test_flush_period_set 30 | assert_equal 2, @appender.flush_period 31 | assert_equal Logging::Appenders::Buffering::DEFAULT_BUFFER_SIZE, @appender.auto_flushing 32 | 33 | @appender.flush_period = '01:30:45' 34 | assert_equal 5445, @appender.flush_period 35 | 36 | @appender.flush_period = '245' 37 | assert_equal 245, @appender.flush_period 38 | 39 | @appender.auto_flushing = true 40 | assert_equal Logging::Appenders::Buffering::DEFAULT_BUFFER_SIZE, @appender.auto_flushing 41 | 42 | @appender.auto_flushing = 200 43 | assert_equal 200, @appender.auto_flushing 44 | end 45 | 46 | def test_async_flusher_running 47 | flusher = @appender.instance_variable_get(:@async_flusher) 48 | assert_instance_of Logging::Appenders::Buffering::AsyncFlusher, flusher 49 | 50 | sleep 0.250 # give the flusher thread another moment to start 51 | assert flusher.waiting?, 'the async flusher should be waiting for a signal' 52 | end 53 | 54 | def test_append 55 | event = Logging::LogEvent.new('TestLogger', @levels['warn'], 56 | [1, 2, 3, 4], false) 57 | @appender.append event 58 | @appender.append event 59 | event.level = @levels['debug'] 60 | event.data = 'the big log message' 61 | @appender.append event 62 | 63 | assert_nil(readline) 64 | sleep 3 65 | 66 | assert_equal " WARN TestLogger : #{[1, 2, 3, 4]}\n", readline 67 | assert_equal " WARN TestLogger : #{[1, 2, 3, 4]}\n", readline 68 | assert_equal "DEBUG TestLogger : the big log message\n", readline 69 | assert_nil(readline) 70 | 71 | @appender.close 72 | assert_raise(RuntimeError) {@appender.append event} 73 | end 74 | 75 | def test_flush_on_close 76 | assert_equal false, @sio.closed? 77 | assert_equal false, @appender.closed? 78 | 79 | event = Logging::LogEvent.new('TestLogger', @levels['warn'], 80 | [1, 2, 3, 4], false) 81 | 82 | @appender.flush_period = "24:00:00" 83 | @appender.append event 84 | event.level = @levels['debug'] 85 | event.data = 'the big log message' 86 | @appender.append event 87 | 88 | assert_nil(readline) 89 | 90 | @appender.close_method = :close_write 91 | @appender.close 92 | assert_equal false, @sio.closed? 93 | assert_equal true, @appender.closed? 94 | 95 | assert_equal " WARN TestLogger : #{[1, 2, 3, 4]}\n", readline 96 | assert_equal "DEBUG TestLogger : the big log message\n", readline 97 | assert_nil(readline) 98 | 99 | @sio.close 100 | assert_equal true, @sio.closed? 101 | end 102 | 103 | def test_auto_flushing 104 | @appender.auto_flushing = 3 105 | 106 | event = Logging::LogEvent.new('TestLogger', @levels['warn'], 107 | [1, 2, 3, 4], false) 108 | 109 | @appender.append event 110 | @appender.append event 111 | event.level = @levels['debug'] 112 | event.data = 'the big log message' 113 | @appender.append event 114 | event.level = @levels['info'] 115 | event.data = 'just FYI' 116 | @appender.append event 117 | event.level = @levels['warn'] 118 | event.data = 'this is your last warning!' 119 | @appender.append event 120 | 121 | assert_equal " WARN TestLogger : #{[1, 2, 3, 4]}\n", readline 122 | assert_equal " WARN TestLogger : #{[1, 2, 3, 4]}\n", readline 123 | assert_equal "DEBUG TestLogger : the big log message\n", readline 124 | 125 | assert_nil(readline) 126 | sleep 3 127 | 128 | assert_equal " INFO TestLogger : just FYI\n", readline 129 | assert_equal " WARN TestLogger : this is your last warning!\n", readline 130 | assert_nil(readline) 131 | end 132 | 133 | def test_setting_flush_period_to_nil 134 | flusher = @appender.instance_variable_get(:@async_flusher) 135 | assert_instance_of Logging::Appenders::Buffering::AsyncFlusher, flusher 136 | 137 | @appender.flush_period = nil 138 | 139 | assert_nil @appender.instance_variable_get(:@async_flusher) 140 | end 141 | 142 | def test_setting_negative_flush_period 143 | assert_raise(ArgumentError) { @appender.flush_period = -1 } 144 | end 145 | 146 | def test_async_writes 147 | @appender.auto_flushing = 3 148 | @appender.flush_period = nil 149 | @appender.async = true 150 | 151 | event = Logging::LogEvent.new('TestLogger', @levels['warn'], 152 | [1, 2, 3, 4], false) 153 | 154 | flusher = @appender.instance_variable_get(:@async_flusher) 155 | assert_instance_of Logging::Appenders::Buffering::AsyncFlusher, flusher 156 | 157 | @appender.append event 158 | assert_nil(readline) 159 | 160 | event.level = @levels['debug'] 161 | event.data = 'the big log message' 162 | @appender.append event 163 | sleep 0.250 164 | assert_nil(readline) 165 | 166 | event.level = @levels['info'] 167 | event.data = 'just FYI' 168 | @appender.append event # might write here, might not 169 | sleep 0.250 # so sleep a little to let the write occur 170 | 171 | assert_equal " WARN TestLogger : #{[1, 2, 3, 4]}\n", readline 172 | assert_equal "DEBUG TestLogger : the big log message\n", readline 173 | assert_equal " INFO TestLogger : just FYI\n", readline 174 | 175 | event.level = @levels['warn'] 176 | event.data = 'this is your last warning!' 177 | @appender.append event 178 | assert_nil(readline) 179 | 180 | @appender.close_method = :close_write 181 | @appender.close 182 | 183 | assert_equal " WARN TestLogger : this is your last warning!\n", readline 184 | 185 | assert_nil @appender.instance_variable_get(:@async_flusher) 186 | end 187 | 188 | private 189 | def readline 190 | @appender.readline 191 | end 192 | end 193 | end 194 | end 195 | -------------------------------------------------------------------------------- /test/layouts/test_json.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('../setup', File.dirname(__FILE__)) 3 | 4 | module TestLogging 5 | module TestLayouts 6 | 7 | class TestJson < Test::Unit::TestCase 8 | include LoggingTestCase 9 | 10 | def setup 11 | super 12 | @layout = Logging.layouts.json({}) 13 | @levels = Logging::LEVELS 14 | @date_fmt = '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}(Z|[+-]\d{2}:\d{2})' 15 | Thread.current[:name] = nil 16 | end 17 | 18 | def test_initializer 19 | assert_raise(ArgumentError) { 20 | Logging.layouts.parseable.new :style => :foo 21 | } 22 | end 23 | 24 | def test_format 25 | event = Logging::LogEvent.new('ArrayLogger', @levels['info'], 26 | 'log message', false) 27 | format = @layout.format(event) 28 | assert_match %r/"timestamp":"#@date_fmt"/, format 29 | assert_match %r/"level":"INFO"/, format 30 | assert_match %r/"logger":"ArrayLogger"/, format 31 | assert_match %r/"message":"log message"/, format 32 | 33 | event.data = [1, 2, 3, 4] 34 | format = @layout.format(event) 35 | assert_match %r/"timestamp":"#@date_fmt"/, format 36 | assert_match %r/"level":"INFO"/, format 37 | assert_match %r/"logger":"ArrayLogger"/, format 38 | assert_match %r/"message":\[1,2,3,4\]/, format 39 | 40 | event.level = @levels['debug'] 41 | event.data = 'and another message' 42 | format = @layout.format(event) 43 | assert_match %r/"timestamp":"#@date_fmt"/, format 44 | assert_match %r/"level":"DEBUG"/, format 45 | assert_match %r/"logger":"ArrayLogger"/, format 46 | assert_match %r/"message":"and another message"/, format 47 | 48 | event.logger = 'Test' 49 | event.level = @levels['fatal'] 50 | event.data = Exception.new 51 | format = @layout.format(event) 52 | assert_match %r/"timestamp":"#@date_fmt"/, format 53 | assert_match %r/"level":"FATAL"/, format 54 | assert_match %r/"logger":"Test"/, format 55 | assert_match %r/"message":\{(?:"(?:class|message)":"Exception",?){2}\}/, format 56 | end 57 | 58 | def test_items 59 | assert_equal %w[timestamp level logger message], @layout.items 60 | end 61 | 62 | def test_items_eq 63 | event = Logging::LogEvent.new('TestLogger', @levels['info'], 64 | ['log message'], false) 65 | 66 | @layout.items = %w[timestamp] 67 | assert_equal %w[timestamp], @layout.items 68 | assert_match %r/\{"timestamp":"#@date_fmt"\}\n/, @layout.format(event) 69 | 70 | # 'foo' is not a recognized item 71 | assert_raise(ArgumentError) { 72 | @layout.items = %w[timestamp logger foo] 73 | } 74 | end 75 | 76 | def test_items_all 77 | event = Logging::LogEvent.new('TestLogger', @levels['info'], 78 | 'log message', false) 79 | event.file = 'test_file.rb' 80 | event.line = 123 81 | event.method_name = 'method_name' 82 | 83 | @layout.items = %w[logger] 84 | assert_equal %Q[{"logger":"TestLogger"}\n], @layout.format(event) 85 | 86 | @layout.items = %w[file] 87 | assert_equal %Q[{"file":"test_file.rb"}\n], @layout.format(event) 88 | 89 | @layout.items = %w[level] 90 | assert_equal %Q[{"level":"INFO"}\n], @layout.format(event) 91 | 92 | @layout.items = %w[line] 93 | assert_equal %Q[{"line":123}\n], @layout.format(event) 94 | 95 | @layout.items = %w[message] 96 | assert_equal %Q[{"message":"log message"}\n], @layout.format(event) 97 | 98 | @layout.items = %w[method] 99 | assert_equal %Q[{"method":"method_name"}\n], @layout.format(event) 100 | 101 | @layout.items = %w[hostname] 102 | assert_equal %Q[{"hostname":"#{Socket.gethostname}"}\n], @layout.format(event) 103 | 104 | @layout.items = %w[pid] 105 | assert_match %r/\A\{"pid":\d+\}\n\z/, @layout.format(event) 106 | 107 | @layout.items = %w[millis] 108 | assert_match %r/\A\{"millis":\d+\}\n\z/, @layout.format(event) 109 | 110 | @layout.items = %w[thread_id] 111 | assert_match %r/\A\{"thread_id":-?\d+\}\n\z/, @layout.format(event) 112 | 113 | @layout.items = %w[thread] 114 | assert_equal %Q[{"thread":null}\n], @layout.format(event) 115 | Thread.current[:name] = "Main" 116 | assert_equal %Q[{"thread":"Main"}\n], @layout.format(event) 117 | 118 | @layout.items = %w[mdc] 119 | assert_match %r/\A\{"mdc":\{\}\}\n\z/, @layout.format(event) 120 | 121 | @layout.items = %w[ndc] 122 | assert_match %r/\A\{"ndc":\[\]\}\n\z/, @layout.format(event) 123 | end 124 | 125 | def test_mdc_output 126 | event = Logging::LogEvent.new('TestLogger', @levels['info'], 127 | 'log message', false) 128 | Logging.mdc['X-Session'] = '123abc' 129 | Logging.mdc['Cookie'] = 'monster' 130 | 131 | @layout.items = %w[timestamp level logger message mdc] 132 | 133 | format = @layout.format(event) 134 | assert_match %r/"timestamp":"#@date_fmt"/, format 135 | assert_match %r/"level":"INFO"/, format 136 | assert_match %r/"logger":"TestLogger"/, format 137 | assert_match %r/"message":"log message"/, format 138 | assert_match %r/"mdc":\{(?:(?:"X-Session":"123abc"|"Cookie":"monster"),?){2}\}/, format 139 | 140 | Logging.mdc.delete 'Cookie' 141 | format = @layout.format(event) 142 | assert_match %r/"mdc":\{"X-Session":"123abc"\}/, format 143 | end 144 | 145 | def test_ndc_output 146 | event = Logging::LogEvent.new('TestLogger', @levels['info'], 147 | 'log message', false) 148 | Logging.ndc << 'context a' 149 | Logging.ndc << 'context b' 150 | 151 | @layout.items = %w[timestamp level logger message ndc] 152 | 153 | format = @layout.format(event) 154 | assert_match %r/"timestamp":"#@date_fmt"/, format 155 | assert_match %r/"level":"INFO"/, format 156 | assert_match %r/"logger":"TestLogger"/, format 157 | assert_match %r/"message":"log message"/, format 158 | assert_match %r/"ndc":\["context a","context b"\]/, format 159 | 160 | Logging.ndc.pop 161 | format = @layout.format(event) 162 | assert_match %r/"ndc":\["context a"\]/, format 163 | 164 | Logging.ndc.pop 165 | format = @layout.format(event) 166 | assert_match %r/"ndc":\[\]/, format 167 | end 168 | 169 | def test_utc_offset 170 | layout = Logging.layouts.json(:items => %w[timestamp]) 171 | event = Logging::LogEvent.new('TimestampLogger', @levels['info'], 'log message', false) 172 | event.time = Time.utc(2016, 12, 1, 12, 0, 0).freeze 173 | 174 | assert_equal %Q/{"timestamp":"2016-12-01T12:00:00.000000Z"}\n/, layout.format(event) 175 | 176 | layout.utc_offset = "-06:00" 177 | assert_equal %Q/{"timestamp":"2016-12-01T06:00:00.000000-06:00"}\n/, layout.format(event) 178 | 179 | layout.utc_offset = "+01:00" 180 | assert_equal %Q/{"timestamp":"2016-12-01T13:00:00.000000+01:00"}\n/, layout.format(event) 181 | end 182 | end # class TestJson 183 | end # module TestLayouts 184 | end # module TestLogging 185 | 186 | -------------------------------------------------------------------------------- /test/appenders/test_buffered_io.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require File.expand_path('../setup', File.dirname(__FILE__)) 4 | 5 | module TestLogging 6 | module TestAppenders 7 | 8 | class TestBufferedIO < Test::Unit::TestCase 9 | include LoggingTestCase 10 | 11 | def setup 12 | super 13 | @appender = Logging.appenders.string_io \ 14 | 'test_appender', 15 | :auto_flushing => 3, 16 | :immediate_at => :error, 17 | :encoding => 'UTF-8' 18 | 19 | @appender.clear 20 | @sio = @appender.sio 21 | @levels = Logging::LEVELS 22 | begin readline rescue EOFError end 23 | end 24 | 25 | def test_append 26 | event = Logging::LogEvent.new('TestLogger', @levels['warn'], 27 | [1, 2, 3, 4], false) 28 | @appender.append event 29 | assert_nil(readline) 30 | 31 | @appender.append event 32 | assert_nil(readline) 33 | 34 | event.level = @levels['debug'] 35 | event.data = 'the big log message' 36 | @appender.append event 37 | 38 | assert_equal " WARN TestLogger : #{[1, 2, 3, 4]}\n", readline 39 | assert_equal " WARN TestLogger : #{[1, 2, 3, 4]}\n", readline 40 | assert_equal "DEBUG TestLogger : the big log message\n", readline 41 | assert_nil(readline) 42 | 43 | @appender.close 44 | assert_raise(RuntimeError) {@appender.append event} 45 | end 46 | 47 | def test_append_with_write_size 48 | event = Logging::LogEvent.new('TestLogger', @levels['warn'], %w[a b c d], false) 49 | @appender.write_size = 2 50 | 51 | @appender.append event 52 | assert_nil(readline) 53 | 54 | @appender.append event 55 | assert_nil(readline) 56 | 57 | event.level = @levels['debug'] 58 | event.data = 'the big log message' 59 | @appender.append event 60 | 61 | assert_equal " WARN TestLogger : #{%w[a b c d]}\n", readline 62 | assert_equal " WARN TestLogger : #{%w[a b c d]}\n", readline 63 | assert_equal "DEBUG TestLogger : the big log message\n", readline 64 | assert_nil(readline) 65 | end 66 | 67 | def test_append_error 68 | # setup an internal logger to capture error messages from the IO 69 | # appender 70 | log = Logging.appenders.string_io('__internal_io') 71 | Logging.logger[Logging].add_appenders(log) 72 | Logging.logger[Logging].level = 'all' 73 | 74 | 75 | # close the string IO object so we get an error 76 | @sio.close 77 | event = Logging::LogEvent.new('TestLogger', @levels['warn'], 78 | [1, 2, 3, 4], false) 79 | @appender.append event 80 | assert_nil(log.readline) 81 | 82 | @appender.append event 83 | assert_nil(log.readline) 84 | 85 | @appender.append event 86 | assert_equal "INFO Logging : appender \"test_appender\" has been disabled", log.readline.strip 87 | assert_equal "ERROR Logging : not opened for writing", log.readline.strip 88 | 89 | assert_equal false, @appender.closed? 90 | assert_equal 5, @appender.level 91 | end 92 | 93 | def test_auto_flushing 94 | assert_raise(ArgumentError) { 95 | @appender.auto_flushing = Object.new 96 | } 97 | 98 | assert_raise(ArgumentError) { 99 | @appender.auto_flushing = -1 100 | } 101 | 102 | @appender.auto_flushing = 0 103 | assert_equal Logging::Appenders::Buffering::DEFAULT_BUFFER_SIZE, @appender.auto_flushing 104 | end 105 | 106 | def test_close 107 | assert_equal false, @sio.closed? 108 | assert_equal false, @appender.closed? 109 | 110 | @appender.close 111 | assert_equal true, @sio.closed? 112 | assert_equal true, @appender.closed? 113 | 114 | [STDIN, STDERR, STDOUT].each do |io| 115 | @appender = Logging.appenders.io('test', io) 116 | @appender.close 117 | assert_equal false, io.closed? 118 | assert_equal true, @appender.closed? 119 | end 120 | end 121 | 122 | def test_concat 123 | @appender << "this is a test message\n" 124 | assert_nil(readline) 125 | 126 | @appender << "this is another message\n" 127 | assert_nil(readline) 128 | 129 | @appender << "some other line\n" 130 | 131 | assert_equal "this is a test message\n", readline 132 | assert_equal "this is another message\n", readline 133 | assert_equal "some other line\n", readline 134 | assert_nil(readline) 135 | 136 | @appender.close 137 | assert_raise(RuntimeError) {@appender << 'message'} 138 | end 139 | 140 | def test_concat_error 141 | # setup an internal logger to capture error messages from the IO 142 | # appender 143 | log = Logging.appenders.string_io('__internal_io') 144 | Logging.logger[Logging].add_appenders(log) 145 | Logging.logger[Logging].level = 'all' 146 | 147 | # close the string IO object so we get an error 148 | @sio.close 149 | @appender << 'oopsy' 150 | assert_nil(log.readline) 151 | 152 | @appender << 'whoopsy' 153 | assert_nil(log.readline) 154 | 155 | @appender << 'pooh' 156 | assert_equal "INFO Logging : appender \"test_appender\" has been disabled", log.readline.strip 157 | assert_equal "ERROR Logging : not opened for writing", log.readline.strip 158 | 159 | # and the appender does not close itself 160 | assert_equal false, @appender.closed? 161 | assert_equal 5, @appender.level 162 | end 163 | 164 | def test_flush 165 | @appender << "this is a test message\n" 166 | assert_nil(readline) 167 | 168 | @appender.flush 169 | assert_equal "this is a test message\n", readline 170 | assert_nil(readline) 171 | end 172 | 173 | def test_clear 174 | @appender << "this is a test message\n" 175 | @appender << "this is another test message\n" 176 | 177 | @appender.clear! 178 | @appender.flush 179 | assert_nil(readline) 180 | end 181 | 182 | def test_immediate_at 183 | event = Logging::LogEvent.new('TestLogger', @levels['warn'], 184 | [1, 2, 3, 4], false) 185 | @appender.append event 186 | assert_nil(readline) 187 | 188 | event.level = @levels['error'] 189 | event.data = 'an error message' 190 | @appender.append event 191 | 192 | assert_equal " WARN TestLogger : #{[1, 2, 3, 4]}\n", readline 193 | assert_equal "ERROR TestLogger : an error message\n", readline 194 | assert_nil(readline) 195 | end 196 | 197 | def test_force_encoding 198 | a = 'ümlaut' 199 | b = 'hello ümlaut'.force_encoding('BINARY') 200 | 201 | event_a = Logging::LogEvent.new('TestLogger', @levels['info'], a, false) 202 | event_b = Logging::LogEvent.new('TestLogger', @levels['info'], b, false) 203 | 204 | @appender.append event_a 205 | @appender.append event_b 206 | assert_nil(readline) 207 | 208 | @appender.append event_a 209 | assert_equal " INFO TestLogger : #{a}\n", readline 210 | assert_equal " INFO TestLogger : #{b.force_encoding('UTF-8')}\n", readline 211 | assert_equal " INFO TestLogger : #{a}\n", readline 212 | assert_nil(readline) 213 | end 214 | 215 | private 216 | def readline 217 | @appender.readline 218 | end 219 | 220 | end # class TestBufferedIO 221 | 222 | end # module TestAppenders 223 | end # module TestLogging 224 | 225 | -------------------------------------------------------------------------------- /lib/logging/repository.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'singleton' 3 | 4 | module Logging 5 | 6 | # The Repository is a hash that stores references to all Loggers 7 | # that have been created. It provides methods to determine parent/child 8 | # relationships between Loggers and to retrieve Loggers from the hash. 9 | # 10 | class Repository 11 | include Singleton 12 | 13 | PATH_DELIMITER = '::' # :nodoc: 14 | 15 | # nodoc: 16 | # 17 | # This is a singleton class -- use the +instance+ method to obtain the 18 | # +Repository+ instance. 19 | # 20 | def initialize 21 | @h = {:root => ::Logging::RootLogger.new} 22 | 23 | # configures the internal logger which is disabled by default 24 | logger = ::Logging::Logger.allocate 25 | logger._setup( 26 | to_key(::Logging), 27 | :parent => @h[:root], 28 | :additive => false, 29 | :level => ::Logging::LEVELS.length # turns this logger off 30 | ) 31 | @h[logger.name] = logger 32 | end 33 | 34 | # call-seq: 35 | # instance[name] 36 | # 37 | # Returns the +Logger+ named _name_. 38 | # 39 | # When _name_ is a +String+ or a +Symbol+ it will be used "as is" to 40 | # retrieve the logger. When _name_ is a +Class+ the class name will be 41 | # used to retrieve the logger. When _name_ is an object the name of the 42 | # object's class will be used to retrieve the logger. 43 | # 44 | # Example: 45 | # 46 | # repo = Repository.instance 47 | # obj = MyClass.new 48 | # 49 | # log1 = repo[obj] 50 | # log2 = repo[MyClass] 51 | # log3 = repo['MyClass'] 52 | # 53 | # log1.object_id == log2.object_id # => true 54 | # log2.object_id == log3.object_id # => true 55 | # 56 | def []( key ) @h[to_key(key)] end 57 | 58 | # call-seq: 59 | # instance[name] = logger 60 | # 61 | # Stores the _logger_ under the given _name_. 62 | # 63 | # When _name_ is a +String+ or a +Symbol+ it will be used "as is" to 64 | # store the logger. When _name_ is a +Class+ the class name will be 65 | # used to store the logger. When _name_ is an object the name of the 66 | # object's class will be used to store the logger. 67 | # 68 | def []=( key, val ) @h[to_key(key)] = val end 69 | 70 | # call-seq: 71 | # fetch( name ) 72 | # 73 | # Returns the +Logger+ named _name_. An +KeyError+ will be raised if 74 | # the logger does not exist. 75 | # 76 | # When _name_ is a +String+ or a +Symbol+ it will be used "as is" to 77 | # retrieve the logger. When _name_ is a +Class+ the class name will be 78 | # used to retrieve the logger. When _name_ is an object the name of the 79 | # object's class will be used to retrieve the logger. 80 | # 81 | def fetch( key ) @h.fetch(to_key(key)) end 82 | 83 | # call-seq: 84 | # has_logger?( name ) 85 | # 86 | # Returns +true+ if the given logger exists in the repository. Returns 87 | # +false+ if this is not the case. 88 | # 89 | # When _name_ is a +String+ or a +Symbol+ it will be used "as is" to 90 | # retrieve the logger. When _name_ is a +Class+ the class name will be 91 | # used to retrieve the logger. When _name_ is an object the name of the 92 | # object's class will be used to retrieve the logger. 93 | # 94 | def has_logger?( key ) @h.has_key?(to_key(key)) end 95 | 96 | # call-seq: 97 | # delete( name ) 98 | # 99 | # Deletes the named logger from the repository. All direct children of the 100 | # logger will have their parent reassigned. So the parent of the logger 101 | # being deleted becomes the new parent of the children. 102 | # 103 | # When _name_ is a +String+ or a +Symbol+ it will be used "as is" to 104 | # remove the logger. When _name_ is a +Class+ the class name will be 105 | # used to remove the logger. When _name_ is an object the name of the 106 | # object's class will be used to remove the logger. 107 | # 108 | # Raises a RuntimeError if you try to delete the root logger. 109 | # Raises an KeyError if the named logger is not found. 110 | def delete( key ) 111 | key = to_key(key) 112 | raise 'the :root logger cannot be deleted' if :root == key 113 | 114 | parent = @h.fetch(key).parent 115 | children(key).each {|c| c.__send__(:parent=, parent)} 116 | @h.delete(key) 117 | end 118 | 119 | # call-seq: 120 | # parent( key ) 121 | # 122 | # Returns the parent logger for the logger identified by _key_ where 123 | # _key_ follows the same identification rules described in 124 | # Repository#[]. A parent is returned regardless of the 125 | # existence of the logger referenced by _key_. 126 | # 127 | # A note about parents - 128 | # 129 | # If you have a class A::B::C, then the parent of C is B, and the parent 130 | # of B is A. Parents are determined by namespace. 131 | # 132 | def parent( key ) 133 | name = parent_name(to_key(key)) 134 | return if name.nil? 135 | @h[name] 136 | end 137 | 138 | # call-seq: 139 | # children( key ) 140 | # 141 | # Returns an array of the children loggers for the logger identified by 142 | # _key_ where _key_ follows the same identification rules described in 143 | # +Repository#[]+. Children are returned regardless of the 144 | # existence of the logger referenced by _key_. 145 | # 146 | def children( parent ) 147 | ary = [] 148 | parent = to_key(parent) 149 | 150 | @h.each_pair do |child,logger| 151 | next if :root == child 152 | ary << logger if parent == parent_name(child) 153 | end 154 | return ary.sort 155 | end 156 | 157 | # call-seq: 158 | # to_key( key ) 159 | # 160 | # Takes the given _key_ and converts it into a form that can be used to 161 | # retrieve a logger from the +Repository+ hash. 162 | # 163 | # When _key_ is a +String+ or a +Symbol+ it will be returned "as is". 164 | # When _key_ is a +Class+ the class name will be returned. When _key_ is 165 | # an object the name of the object's class will be returned. 166 | # 167 | def to_key( key ) 168 | case key 169 | when :root, 'root'; :root 170 | when String; key 171 | when Symbol; key.to_s 172 | when Module; key.logger_name 173 | when Object; key.class.logger_name 174 | end 175 | end 176 | 177 | # Returns the name of the parent for the logger identified by the given 178 | # _key_. If the _key_ is for the root logger, then +nil+ is returned. 179 | # 180 | def parent_name( key ) 181 | return if :root == key 182 | 183 | a = key.split PATH_DELIMITER 184 | p = :root 185 | while a.slice!(-1) and !a.empty? 186 | k = a.join PATH_DELIMITER 187 | if @h.has_key? k then p = k; break end 188 | end 189 | p 190 | end 191 | 192 | # :stopdoc: 193 | def self.reset 194 | if defined?(@singleton__instance__) 195 | @singleton__mutex__.synchronize { 196 | @singleton__instance__ = nil 197 | } 198 | else 199 | @__instance__ = nil 200 | class << self 201 | nonce = class << Singleton; self; end 202 | if defined?(nonce::FirstInstanceCall) 203 | define_method(:instance, nonce::FirstInstanceCall) 204 | else 205 | remove_method(:instance) 206 | Singleton.__init__(::Logging::Repository) 207 | end 208 | end 209 | end 210 | return nil 211 | end 212 | # :startdoc: 213 | 214 | end # class Repository 215 | end # module Logging 216 | 217 | -------------------------------------------------------------------------------- /lib/logging/appenders/syslog.rb: -------------------------------------------------------------------------------- 1 | 2 | # only load this class if we have the syslog library 3 | # Windows does not have syslog 4 | # 5 | if HAVE_SYSLOG 6 | 7 | module Logging::Appenders 8 | 9 | # Accessor / Factory for the Syslog appender. 10 | # 11 | def self.syslog( *args ) 12 | fail ArgumentError, '::Logging::Appenders::Syslog needs a name as first argument.' if args.empty? 13 | ::Logging::Appenders::Syslog.new(*args) 14 | end 15 | 16 | # This class provides an Appender that can write to the UNIX syslog 17 | # daemon. 18 | # 19 | class Syslog < ::Logging::Appender 20 | include ::Syslog::Constants 21 | 22 | # call-seq: 23 | # Syslog.new( name, opts = {} ) 24 | # 25 | # Create an appender that will log messages to the system message 26 | # logger. The message is then written to the system console, log files, 27 | # logged-in users, or forwarded to other machines as appropriate. The 28 | # options that can be used to configure the appender are as follows: 29 | # 30 | # :ident => identifier string (name is used by default) 31 | # :logopt => options used when opening the connection 32 | # :facility => the syslog facility to use 33 | # 34 | # The parameter :ident is a string that will be prepended to every 35 | # message. The :logopt argument is a bit field specifying logging 36 | # options, which is formed by OR'ing one or more of the following 37 | # values: 38 | # 39 | # LOG_CONS If syslog() cannot pass the message to syslogd(8) it 40 | # wil attempt to write the message to the console 41 | # ('/dev/console'). 42 | # 43 | # LOG_NDELAY Open the connection to syslogd(8) immediately. Normally 44 | # the open is delayed until the first message is logged. 45 | # Useful for programs that need to manage the order in 46 | # which file descriptors are allocated. 47 | # 48 | # LOG_PERROR Write the message to standard error output as well to 49 | # the system log. Not available on Solaris. 50 | # 51 | # LOG_PID Log the process id with each message: useful for 52 | # identifying instantiations of daemons. 53 | # 54 | # The :facility parameter encodes a default facility to be assigned to 55 | # all messages that do not have an explicit facility encoded: 56 | # 57 | # LOG_AUTH The authorization system: login(1), su(1), getty(8), 58 | # etc. 59 | # 60 | # LOG_AUTHPRIV The same as LOG_AUTH, but logged to a file readable 61 | # only by selected individuals. 62 | # 63 | # LOG_CONSOLE Messages written to /dev/console by the kernel console 64 | # output driver. 65 | # 66 | # LOG_CRON The cron daemon: cron(8). 67 | # 68 | # LOG_DAEMON System daemons, such as routed(8), that are not 69 | # provided for explicitly by other facilities. 70 | # 71 | # LOG_FTP The file transfer protocol daemons: ftpd(8), tftpd(8). 72 | # 73 | # LOG_KERN Messages generated by the kernel. These cannot be 74 | # generated by any user processes. 75 | # 76 | # LOG_LPR The line printer spooling system: lpr(1), lpc(8), 77 | # lpd(8), etc. 78 | # 79 | # LOG_MAIL The mail system. 80 | # 81 | # LOG_NEWS The network news system. 82 | # 83 | # LOG_SECURITY Security subsystems, such as ipfw(4). 84 | # 85 | # LOG_SYSLOG Messages generated internally by syslogd(8). 86 | # 87 | # LOG_USER Messages generated by random user processes. This is 88 | # the default facility identifier if none is specified. 89 | # 90 | # LOG_UUCP The uucp system. 91 | # 92 | # LOG_LOCAL0 Reserved for local use. Similarly for LOG_LOCAL1 93 | # through LOG_LOCAL7. 94 | # 95 | def initialize( name, opts = {} ) 96 | @ident = opts.fetch(:ident, name) 97 | @logopt = Integer(opts.fetch(:logopt, (LOG_PID | LOG_CONS))) 98 | @facility = Integer(opts.fetch(:facility, LOG_USER)) 99 | @syslog = ::Syslog.open(@ident, @logopt, @facility) 100 | 101 | # provides a mapping from the default Logging levels 102 | # to the syslog levels 103 | @map = [LOG_DEBUG, LOG_INFO, LOG_WARNING, LOG_ERR, LOG_CRIT] 104 | 105 | map = opts.fetch(:map, nil) 106 | self.map = map unless map.nil? 107 | 108 | super 109 | end 110 | 111 | # call-seq: 112 | # map = { logging_levels => syslog_levels } 113 | # 114 | # Configure the mapping from the Logging levels to the syslog levels. 115 | # This is needed in order to log events at the proper syslog level. 116 | # 117 | # Without any configuration, the following mapping will be used: 118 | # 119 | # :debug => LOG_DEBUG 120 | # :info => LOG_INFO 121 | # :warn => LOG_WARNING 122 | # :error => LOG_ERR 123 | # :fatal => LOG_CRIT 124 | # 125 | def map=( levels ) 126 | map = [] 127 | levels.keys.each do |lvl| 128 | num = ::Logging.level_num(lvl) 129 | map[num] = syslog_level_num(levels[lvl]) 130 | end 131 | @map = map 132 | end 133 | 134 | # call-seq: 135 | # close 136 | # 137 | # Closes the connection to the syslog facility. 138 | # 139 | def close( footer = true ) 140 | super 141 | @syslog.close if @syslog.opened? 142 | self 143 | end 144 | 145 | # call-seq: 146 | # closed? => true or false 147 | # 148 | # Queries the connection to the syslog facility and returns +true+ if 149 | # the connection is closed. 150 | # 151 | def closed? 152 | !@syslog.opened? 153 | end 154 | 155 | # Reopen the connection to the underlying logging destination. If the 156 | # connection is currently closed then it will be opened. If the connection 157 | # is currently open then it will be closed and immediately opened. 158 | # 159 | def reopen 160 | @mutex.synchronize { 161 | if @syslog.opened? 162 | flush 163 | @syslog.close 164 | end 165 | @syslog = ::Syslog.open(@ident, @logopt, @facility) 166 | } 167 | super 168 | self 169 | end 170 | 171 | 172 | private 173 | 174 | # call-seq: 175 | # write( event ) 176 | # 177 | # Write the given _event_ to the syslog facility. The log event will be 178 | # processed through the Layout associated with this appender. The message 179 | # will be logged at the level specified by the event. 180 | # 181 | def write( event ) 182 | pri = LOG_DEBUG 183 | message = if event.instance_of?(::Logging::LogEvent) 184 | pri = @map[event.level] 185 | @layout.format(event) 186 | else 187 | event.to_s 188 | end 189 | return if message.empty? 190 | 191 | @mutex.synchronize { @syslog.log(pri, '%s', message) } 192 | self 193 | end 194 | 195 | # call-seq: 196 | # syslog_level_num( level ) => integer 197 | # 198 | # Takes the given _level_ as a string, symbol, or integer and returns 199 | # the corresponding syslog level number. 200 | # 201 | def syslog_level_num( level ) 202 | case level 203 | when Integer; level 204 | when String, Symbol 205 | level = level.to_s.upcase 206 | self.class.const_get level 207 | else 208 | raise ArgumentError, "unknown level '#{level}'" 209 | end 210 | end 211 | 212 | end # Syslog 213 | end # Logging::Appenders 214 | end # HAVE_SYSLOG 215 | -------------------------------------------------------------------------------- /lib/logging/layout.rb: -------------------------------------------------------------------------------- 1 | 2 | module Logging 3 | 4 | # The +Layout+ class provides methods for formatting log events into a 5 | # string representation. Layouts are used by Appenders to format log 6 | # events before writing them to the logging destination. 7 | # 8 | # All other Layouts inherit from this class which provides stub methods. 9 | # Each subclass should provide a +format+ method. A layout can be used by 10 | # more than one +Appender+ so all the methods need to be thread safe. 11 | # 12 | class Layout 13 | 14 | # call-seq: 15 | # Layout.new( :format_as => :string ) 16 | # 17 | # Creates a new layout that will format objects as strings using the 18 | # given :format_as style. This can be one of :string, 19 | # :inspect, or :yaml. These formatting commands map to 20 | # the following object methods: 21 | # 22 | # * :string => to_s 23 | # * :inspect => inspect 24 | # * :yaml => to_yaml 25 | # * :json => MultiJson.encode(obj) 26 | # 27 | # If the format is not specified then the global object format is used 28 | # (see Logging#format_as). If the global object format is not specified 29 | # then :string is used. 30 | # 31 | def initialize( opts = {} ) 32 | ::Logging.init unless ::Logging.initialized? 33 | 34 | default = ::Logging.const_defined?('OBJ_FORMAT') ? 35 | ::Logging::OBJ_FORMAT : nil 36 | 37 | f = opts.fetch(:format_as, default) 38 | f = f.intern if f.instance_of? String 39 | 40 | @obj_format = case f 41 | when :inspect, :yaml, :json; f 42 | else :string end 43 | 44 | self.backtrace = opts.fetch(:backtrace, ::Logging.backtrace) 45 | self.utc_offset = opts.fetch(:utc_offset, ::Logging.utc_offset) 46 | self.cause_depth = opts.fetch(:cause_depth, ::Logging.cause_depth) 47 | end 48 | 49 | # call-seq: 50 | # layout.backtrace = true 51 | # 52 | # Set the backtrace flag to the given value. This can be set to `true` or 53 | # `false`. 54 | # 55 | def backtrace=( value ) 56 | @backtrace = case value 57 | when :on, 'on', true; true 58 | when :off, 'off', false; false 59 | else 60 | raise ArgumentError, "backtrace must be `true` or `false`" 61 | end 62 | end 63 | 64 | # Returns the backtrace setting. 65 | attr_reader :backtrace 66 | alias :backtrace? :backtrace 67 | 68 | # Set the UTC offset used when formatting time values. If left unset, the 69 | # default local time zone will be used for time values. This method accepts 70 | # the `utc_offset` format supported by the `Time#localtime` method in Ruby. 71 | # 72 | # Passing "UTC" or `0` as the UTC offset will cause all times to be reported 73 | # in the UTC timezone. 74 | # 75 | # layout.utc_offset = "-07:00" # Mountain Standard Time in North America 76 | # layout.utc_offset = "+01:00" # Central European Time 77 | # layout.utc_offset = "UTC" # UTC 78 | # layout.utc_offset = 0 # UTC 79 | # 80 | def utc_offset=( value ) 81 | @utc_offset = case value 82 | when nil; nil 83 | when "UTC", "GMT", 0; 0 84 | else 85 | Time.now.localtime(value) 86 | value 87 | end 88 | end 89 | 90 | # Returns the UTC offset. 91 | attr_reader :utc_offset 92 | 93 | # 94 | # 95 | def cause_depth=( value ) 96 | if value.nil? 97 | @cause_depth = ::Logging::DEFAULT_CAUSE_DEPTH 98 | else 99 | value = Integer(value) 100 | @cause_depth = value < 0 ? ::Logging::DEFAULT_CAUSE_DEPTH : value 101 | end 102 | end 103 | 104 | # Returns the exception cause depth formatting limit. 105 | attr_reader :cause_depth 106 | 107 | # Internal: Helper method that applies the UTC offset to the given `time` 108 | # instance. A new Time is returned that is equivalent to the original `time` 109 | # but pinned to the timezone given by the UTC offset. 110 | # 111 | # If a UTC offset has not been set, then the original `time` instance is 112 | # returned unchanged. 113 | # 114 | def apply_utc_offset( time ) 115 | return time if utc_offset.nil? 116 | 117 | time = time.dup 118 | if utc_offset == 0 119 | time.utc 120 | else 121 | time.localtime(utc_offset) 122 | end 123 | time 124 | end 125 | 126 | # call-seq: 127 | # format( event ) 128 | # 129 | # Returns a string representation of the given logging _event_. It is 130 | # up to subclasses to implement this method. 131 | # 132 | def format( event ) nil end 133 | 134 | # call-seq: 135 | # header 136 | # 137 | # Returns a header string to be used at the beginning of a logging 138 | # appender. 139 | # 140 | def header( ) '' end 141 | 142 | # call-seq: 143 | # footer 144 | # 145 | # Returns a footer string to be used at the end of a logging appender. 146 | # 147 | def footer( ) '' end 148 | 149 | # call-seq: 150 | # format_obj( obj ) 151 | # 152 | # Return a string representation of the given object. Depending upon 153 | # the configuration of the logger system the format will be an +inspect+ 154 | # based representation or a +yaml+ based representation. 155 | # 156 | def format_obj( obj ) 157 | case obj 158 | when String; obj 159 | when Exception 160 | lines = ["<#{obj.class.name}> #{obj.message}"] 161 | lines.concat(obj.backtrace) if backtrace? && obj.backtrace 162 | format_cause(obj, lines) 163 | lines.join("\n\t") 164 | when nil; "<#{obj.class.name}> nil" 165 | else 166 | str = "<#{obj.class.name}> " 167 | str << case @obj_format 168 | when :inspect; obj.inspect 169 | when :yaml; try_yaml(obj) 170 | when :json; try_json(obj) 171 | else obj.to_s end 172 | str 173 | end 174 | end 175 | 176 | # Internal: Format any nested exceptions found in the given exception `e` 177 | # while respecting the maximum `cause_depth`. The lines array is used to 178 | # capture all the output lines form the nested exceptions; the array is later 179 | # joined by the `format_obj` method. 180 | # 181 | # e - Exception to format 182 | # lines - Array of output lines 183 | # 184 | # Returns the input `lines` Array 185 | def format_cause(e, lines) 186 | return lines if cause_depth == 0 187 | 188 | cause_depth.times do 189 | break unless e.respond_to?(:cause) && e.cause 190 | 191 | cause = e.cause 192 | lines << "--- Caused by ---" 193 | lines << "<#{cause.class.name}> #{cause.message}" 194 | lines.concat(format_cause_backtrace(e, cause)) if backtrace? && cause.backtrace 195 | 196 | e = cause 197 | end 198 | 199 | if e.respond_to?(:cause) && e.cause 200 | lines << "--- Further #cause backtraces were omitted ---" 201 | end 202 | 203 | lines 204 | end 205 | 206 | # Internal: Format the backtrace of the nested `cause` but remove the common 207 | # exception lines from the parent exception. This helps keep the backtraces a 208 | # wee bit shorter and more comprehensible. 209 | # 210 | # e - parent exception 211 | # cause - the nested exception generating the returned backtrace 212 | # 213 | # Returns an Array of backtracke lines. 214 | def format_cause_backtrace(e, cause) 215 | # Find where the cause's backtrace differs from the parent exception's. 216 | backtrace = Array(e.backtrace) 217 | cause_backtrace = Array(cause.backtrace) 218 | index = -1 219 | min_index = [backtrace.size, cause_backtrace.size].min * -1 220 | just_in_case = -5000 221 | 222 | while index > min_index && backtrace[index] == cause_backtrace[index] && index >= just_in_case 223 | index -= 1 224 | end 225 | 226 | # Add on a few common frames to make it clear where the backtraces line up. 227 | index += 3 228 | index = -1 if index >= 0 229 | 230 | cause_backtrace[0..index] 231 | end 232 | 233 | 234 | # Attempt to format the _obj_ using yaml, but fall back to inspect style 235 | # formatting if yaml fails. 236 | # 237 | # obj - The Object to format. 238 | # 239 | # Returns a String representation of the object. 240 | # 241 | def try_yaml( obj ) 242 | "\n#{obj.to_yaml}" 243 | rescue TypeError 244 | obj.inspect 245 | end 246 | 247 | # Attempt to format the given object as a JSON string, but fall back to 248 | # inspect formatting if JSON encoding fails. 249 | # 250 | # obj - The Object to format. 251 | # 252 | # Returns a String representation of the object. 253 | # 254 | def try_json( obj ) 255 | MultiJson.encode(obj) 256 | rescue StandardError 257 | obj.inspect 258 | end 259 | end 260 | end 261 | -------------------------------------------------------------------------------- /test/layouts/test_pattern.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('../../setup', __FILE__) 3 | 4 | module TestLogging 5 | module TestLayouts 6 | 7 | class TestPattern < Test::Unit::TestCase 8 | include LoggingTestCase 9 | 10 | def setup 11 | super 12 | @layout = Logging.layouts.pattern({}) 13 | @levels = Logging::LEVELS 14 | @date_fmt = '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}' 15 | Thread.current[:name] = nil 16 | end 17 | 18 | def test_date_method 19 | assert_nil @layout.date_method 20 | end 21 | 22 | def test_date_method_eq 23 | @layout.date_method = :to_f 24 | assert_equal :to_f, @layout.date_method 25 | assert_instance_of Float, @layout.format_date(Time.now) 26 | 27 | @layout.date_method = 'usec' 28 | assert_equal 'usec', @layout.date_method 29 | assert_kind_of Integer, @layout.format_date(Time.now) 30 | 31 | @layout.date_method = :to_s 32 | assert_equal :to_s, @layout.date_method 33 | assert_instance_of String, @layout.format_date(Time.now) 34 | 35 | # now, even if we have defined a date_pattern, the date_method should 36 | # supersede the date_pattern 37 | @layout.date_pattern = '%Y' 38 | 39 | @layout.date_method = 'usec' 40 | assert_equal 'usec', @layout.date_method 41 | assert_kind_of Integer, @layout.format_date(Time.now) 42 | end 43 | 44 | def test_date_pattern 45 | assert_equal '%Y-%m-%dT%H:%M:%S', @layout.date_pattern 46 | end 47 | 48 | def test_date_pattern_eq 49 | @layout.date_pattern = '%Y' 50 | assert_equal '%Y', @layout.date_pattern 51 | assert_match %r/\A\d{4}\z/, @layout.format_date(Time.now) 52 | 53 | @layout.date_pattern = '%H:%M' 54 | assert_equal '%H:%M', @layout.date_pattern 55 | assert_match %r/\A\d{2}:\d{2}\z/, @layout.format_date(Time.now) 56 | end 57 | 58 | def test_format 59 | fmt = '\[' + @date_fmt + '\] %s -- %s : %s\n' 60 | 61 | event = Logging::LogEvent.new('ArrayLogger', @levels['info'], 62 | 'log message', false) 63 | rgxp = Regexp.new(sprintf(fmt, 'INFO ', 'ArrayLogger', 'log message')) 64 | assert_match rgxp, @layout.format(event) 65 | 66 | event.data = [1, 2, 3, 4] 67 | rgxp = Regexp.new(sprintf(fmt, 'INFO ', 'ArrayLogger', 68 | Regexp.escape(" #{[1,2,3,4]}"))) 69 | assert_match rgxp, @layout.format(event) 70 | 71 | event.level = @levels['debug'] 72 | event.data = 'and another message' 73 | rgxp = Regexp.new( 74 | sprintf(fmt, 'DEBUG', 'ArrayLogger', 'and another message')) 75 | assert_match rgxp, @layout.format(event) 76 | 77 | event.logger = 'Test' 78 | event.level = @levels['fatal'] 79 | event.data = Exception.new 80 | rgxp = Regexp.new( 81 | sprintf(fmt, 'FATAL', 'Test', ' Exception')) 82 | assert_match rgxp, @layout.format(event) 83 | end 84 | 85 | def test_format_date 86 | rgxp = Regexp.new @date_fmt 87 | assert_match rgxp, @layout.format_date(Time.now) 88 | end 89 | 90 | def test_pattern 91 | assert_equal "[%d] %-5l -- %c : %m\n", @layout.pattern 92 | end 93 | 94 | def test_pattern_eq 95 | event = Logging::LogEvent.new('TestLogger', @levels['info'], 96 | ['log message'], false) 97 | 98 | @layout.pattern = '%d' 99 | assert_equal '%d', @layout.pattern 100 | assert_match Regexp.new(@date_fmt), @layout.format(event) 101 | end 102 | 103 | def test_pattern_all 104 | event = Logging::LogEvent.new('TestLogger', @levels['info'], 105 | 'log message', false) 106 | event.file = 'test_file.rb' 107 | event.line = '123' 108 | event.method_name = 'method_name' 109 | 110 | @layout.pattern = '%c' 111 | assert_equal 'TestLogger', @layout.format(event) 112 | 113 | @layout.pattern = '%d' 114 | assert_match Regexp.new(@date_fmt), @layout.format(event) 115 | 116 | @layout.pattern = '%F' 117 | assert_equal 'test_file.rb', @layout.format(event) 118 | 119 | @layout.pattern = '%l' 120 | assert_equal 'INFO', @layout.format(event) 121 | 122 | @layout.pattern = '%L' 123 | assert_equal '123', @layout.format(event) 124 | 125 | @layout.pattern = '%m' 126 | assert_equal 'log message', @layout.format(event) 127 | 128 | @layout.pattern = '%M' 129 | assert_equal 'method_name', @layout.format(event) 130 | 131 | @layout.pattern = '%p' 132 | assert_match %r/\A\d+\z/, @layout.format(event) 133 | 134 | @layout.pattern = '%r' 135 | assert_match %r/\A\d+\z/, @layout.format(event) 136 | 137 | @layout.pattern = '%t' 138 | assert_match %r/\A-?\d+\z/, @layout.format(event) 139 | 140 | @layout.pattern = '%T' 141 | assert_equal "", @layout.format(event) 142 | Thread.current[:name] = "Main" 143 | assert_equal "Main", @layout.format(event) 144 | 145 | @layout.pattern = '%h' 146 | hostname = Socket.gethostname 147 | assert_equal hostname, @layout.format(event) 148 | 149 | @layout.pattern = '%%' 150 | assert_equal '%', @layout.format(event) 151 | 152 | # 'z' is not a recognized format character 153 | assert_raise(ArgumentError) { 154 | @layout.pattern = '[%d] %% %c - %l %z blah' 155 | } 156 | assert_equal '%', @layout.format(event) 157 | 158 | @layout.pattern = '%5l' 159 | assert_equal ' INFO', @layout.format(event) 160 | 161 | @layout.pattern = '%-5l' 162 | assert_equal 'INFO ', @layout.format(event) 163 | 164 | @layout.pattern = '%.1l, %c' 165 | assert_equal 'I, TestLogger', @layout.format(event) 166 | 167 | @layout.pattern = '%7.7m' 168 | assert_equal 'log mes', @layout.format(event) 169 | 170 | event.data = 'tim' 171 | assert_equal ' tim', @layout.format(event) 172 | 173 | @layout.pattern = '%-7.7m' 174 | assert_equal 'tim ', @layout.format(event) 175 | end 176 | 177 | def test_pattern_logger_name_precision 178 | event = Logging::LogEvent.new('Foo', @levels['info'], 'message', false) 179 | 180 | @layout.pattern = '%c{2}' 181 | assert_equal 'Foo', @layout.format(event) 182 | 183 | event.logger = 'Foo::Bar::Baz::Buz' 184 | assert_equal 'Baz::Buz', @layout.format(event) 185 | 186 | assert_raise(ArgumentError) { 187 | @layout.pattern = '%c{0}' 188 | } 189 | 190 | @layout.pattern = '%c{foo}' 191 | event.logger = 'Foo::Bar' 192 | assert_equal 'Foo::Bar{foo}', @layout.format(event) 193 | 194 | @layout.pattern = '%m{42}' 195 | assert_equal 'message{42}', @layout.format(event) 196 | end 197 | 198 | def test_pattern_mdc 199 | @layout.pattern = 'S:%X{X-Session} C:%X{Cookie}' 200 | event = Logging::LogEvent.new('TestLogger', @levels['info'], 'log message', false) 201 | 202 | Logging.mdc['X-Session'] = '123abc' 203 | Logging.mdc['Cookie'] = 'monster' 204 | assert_equal 'S:123abc C:monster', @layout.format(event) 205 | 206 | Logging.mdc.delete 'Cookie' 207 | assert_equal 'S:123abc C:', @layout.format(event) 208 | 209 | Logging.mdc.delete 'X-Session' 210 | assert_equal 'S: C:', @layout.format(event) 211 | end 212 | 213 | def test_pattern_mdc_requires_key_name 214 | assert_raise(ArgumentError) { @layout.pattern = '%X' } 215 | assert_raise(ArgumentError) { @layout.pattern = '%X{}' } 216 | end 217 | 218 | def test_pattern_ndc 219 | @layout.pattern = '%x' 220 | event = Logging::LogEvent.new('TestLogger', @levels['info'], 'log message', false) 221 | 222 | Logging.ndc << 'context a' 223 | Logging.ndc << 'context b' 224 | assert_equal 'context a context b', @layout.format(event) 225 | 226 | @layout.pattern = '%x{, }' 227 | assert_equal 'context a, context b', @layout.format(event) 228 | 229 | Logging.ndc.pop 230 | assert_equal 'context a', @layout.format(event) 231 | end 232 | 233 | def test_utc_offset 234 | layout = Logging.layouts.pattern(:pattern => "%d", :utc_offset => "UTC") 235 | event = Logging::LogEvent.new('DateLogger', @levels['info'], 'log message', false) 236 | event.time = Time.utc(2016, 12, 1, 12, 0, 0).freeze 237 | 238 | assert_equal "2016-12-01T12:00:00", layout.format(event) 239 | 240 | layout.utc_offset = "-06:00" 241 | assert_equal "2016-12-01T06:00:00", layout.format(event) 242 | 243 | layout.utc_offset = "+01:00" 244 | assert_equal "2016-12-01T13:00:00", layout.format(event) 245 | end 246 | end # TestBasic 247 | end # TestLayouts 248 | end # TestLogging 249 | 250 | -------------------------------------------------------------------------------- /test/test_logging.rb: -------------------------------------------------------------------------------- 1 | 2 | require File.expand_path('../setup', __FILE__) 3 | 4 | module TestLogging 5 | 6 | class TestLogging < Test::Unit::TestCase 7 | include LoggingTestCase 8 | 9 | def setup 10 | super 11 | @levels = ::Logging::LEVELS 12 | @lnames = ::Logging::LNAMES 13 | 14 | @fn = File.join(@tmpdir, 'test.log') 15 | @glob = File.join(@tmpdir, '*.log') 16 | end 17 | 18 | def test_backtrace 19 | assert_equal true, ::Logging.backtrace 20 | 21 | assert_equal false, ::Logging.backtrace('off') 22 | assert_equal false, ::Logging.backtrace 23 | 24 | assert_equal true, ::Logging.backtrace('on') 25 | assert_equal true, ::Logging.backtrace 26 | 27 | assert_equal false, ::Logging.backtrace(:off) 28 | assert_equal false, ::Logging.backtrace 29 | 30 | assert_equal true, ::Logging.backtrace(:on) 31 | assert_equal true, ::Logging.backtrace 32 | 33 | assert_equal false, ::Logging.backtrace(false) 34 | assert_equal false, ::Logging.backtrace 35 | 36 | assert_equal true, ::Logging.backtrace(true) 37 | assert_equal true, ::Logging.backtrace 38 | 39 | assert_raise(ArgumentError) {::Logging.backtrace 'foo'} 40 | end 41 | 42 | def test_utc_offset 43 | assert_nil ::Logging.utc_offset 44 | 45 | ::Logging.utc_offset = 0 46 | assert_equal 0, ::Logging.utc_offset 47 | 48 | ::Logging.utc_offset = "UTC" 49 | assert_equal 0, ::Logging.utc_offset 50 | 51 | ::Logging.utc_offset = "+01:00" 52 | assert_equal "+01:00", ::Logging.utc_offset 53 | 54 | assert_raise(ArgumentError) {::Logging.utc_offset = "06:00"} 55 | end 56 | 57 | def test_cause_depth 58 | assert_equal ::Logging::DEFAULT_CAUSE_DEPTH, ::Logging.cause_depth 59 | 60 | ::Logging.cause_depth = 0 61 | assert_equal 0, ::Logging.cause_depth 62 | 63 | ::Logging.cause_depth = nil 64 | assert_equal ::Logging::DEFAULT_CAUSE_DEPTH, ::Logging.cause_depth 65 | 66 | ::Logging.cause_depth = "1024" 67 | assert_equal 1024, ::Logging.cause_depth 68 | 69 | ::Logging.cause_depth = -1 70 | assert_equal ::Logging::DEFAULT_CAUSE_DEPTH, ::Logging.cause_depth 71 | 72 | assert_raise(ArgumentError) {::Logging.cause_depth = "foo"} 73 | end 74 | 75 | def test_basepath 76 | assert_nil ::Logging.basepath 77 | 78 | ::Logging.basepath = "" 79 | assert_nil ::Logging.basepath 80 | 81 | ::Logging.basepath = "./" 82 | assert_equal File.expand_path("../../", __FILE__), ::Logging.basepath 83 | 84 | ::Logging.reset 85 | assert_nil ::Logging.basepath 86 | end 87 | 88 | def test_logger 89 | assert_raise(TypeError) {::Logging.logger []} 90 | 91 | logger = ::Logging.logger STDOUT 92 | assert_match %r/\A-?\d+\z/, logger.name 93 | assert_same logger, ::Logging.logger(STDOUT) 94 | 95 | logger.close 96 | assert !STDOUT.closed? 97 | 98 | assert !File.exist?(@fn) 99 | fd = File.new @fn, 'w' 100 | logger = ::Logging.logger fd, 2, 100 101 | assert_equal @fn, logger.name 102 | logger.debug 'this is a debug message' 103 | logger.warn 'this is a warning message' 104 | logger.error 'and now we should have over 100 bytes of data ' + 105 | 'in the log file' 106 | logger.info 'but the log file should not roll since we provided ' + 107 | 'a file descriptor -- not a file name' 108 | logger.close 109 | assert fd.closed? 110 | assert File.exist?(@fn) 111 | assert_equal 1, Dir.glob(@glob).length 112 | 113 | FileUtils.rm_f @fn 114 | assert !File.exist?(@fn) 115 | logger = ::Logging.logger @fn, 2, 100 116 | assert File.exist?(@fn) 117 | assert_equal @fn, logger.name 118 | logger.debug 'this is a debug message' 119 | logger.warn 'this is a warning message' 120 | logger.error 'and now we should have over 100 bytes of data ' + 121 | 'in the log file' 122 | logger.info 'but the log file should not roll since we provided ' + 123 | 'a file descriptor -- not a file name' 124 | logger.close 125 | assert_equal 3, Dir.glob(@glob).length 126 | end 127 | 128 | def test_init_default 129 | assert_equal({}, @levels) 130 | assert_equal([], @lnames) 131 | assert_same false, ::Logging.initialized? 132 | 133 | ::Logging::Repository.instance 134 | 135 | assert_equal 5, @levels.length 136 | assert_equal 5, @lnames.length 137 | assert_equal 5, ::Logging::MAX_LEVEL_LENGTH 138 | 139 | assert_equal 0, @levels['debug'] 140 | assert_equal 1, @levels['info'] 141 | assert_equal 2, @levels['warn'] 142 | assert_equal 3, @levels['error'] 143 | assert_equal 4, @levels['fatal'] 144 | 145 | assert_equal 'DEBUG', @lnames[0] 146 | assert_equal 'INFO', @lnames[1] 147 | assert_equal 'WARN', @lnames[2] 148 | assert_equal 'ERROR', @lnames[3] 149 | assert_equal 'FATAL', @lnames[4] 150 | end 151 | 152 | def test_init_special 153 | assert_equal({}, @levels) 154 | assert_equal([], @lnames) 155 | assert_same false, ::Logging.initialized? 156 | 157 | assert_raise(ArgumentError) {::Logging.init(1, 2, 3, 4)} 158 | 159 | ::Logging.init :one, 'two', :THREE, 'FoUr', :sIx 160 | 161 | assert_equal 5, @levels.length 162 | assert_equal 5, @lnames.length 163 | assert_equal 5, ::Logging::MAX_LEVEL_LENGTH 164 | 165 | assert_equal 0, @levels['one'] 166 | assert_equal 1, @levels['two'] 167 | assert_equal 2, @levels['three'] 168 | assert_equal 3, @levels['four'] 169 | assert_equal 4, @levels['six'] 170 | 171 | assert_equal 'ONE', @lnames[0] 172 | assert_equal 'TWO', @lnames[1] 173 | assert_equal 'THREE', @lnames[2] 174 | assert_equal 'FOUR', @lnames[3] 175 | assert_equal 'SIX', @lnames[4] 176 | end 177 | 178 | def test_init_all_off 179 | assert_equal({}, @levels) 180 | assert_equal([], @lnames) 181 | assert_same false, ::Logging.initialized? 182 | 183 | ::Logging.init %w(a b all c off d) 184 | 185 | assert_equal 4, @levels.length 186 | assert_equal 4, @lnames.length 187 | assert_equal 3, ::Logging::MAX_LEVEL_LENGTH 188 | 189 | assert_equal 0, @levels['a'] 190 | assert_equal 1, @levels['b'] 191 | assert_equal 2, @levels['c'] 192 | assert_equal 3, @levels['d'] 193 | 194 | assert_equal 'A', @lnames[0] 195 | assert_equal 'B', @lnames[1] 196 | assert_equal 'C', @lnames[2] 197 | assert_equal 'D', @lnames[3] 198 | end 199 | 200 | def test_format_as 201 | assert_equal false, ::Logging.const_defined?('OBJ_FORMAT') 202 | 203 | assert_raises(ArgumentError) {::Logging.format_as 'bob'} 204 | assert_raises(ArgumentError) {::Logging.format_as String} 205 | assert_raises(ArgumentError) {::Logging.format_as :what?} 206 | 207 | remove_const = lambda do |const| 208 | ::Logging.class_eval {remove_const const if const_defined? const} 209 | end 210 | 211 | ::Logging.format_as :string 212 | assert ::Logging.const_defined?('OBJ_FORMAT') 213 | assert_equal :string, ::Logging::OBJ_FORMAT 214 | remove_const[:OBJ_FORMAT] 215 | 216 | ::Logging.format_as :inspect 217 | assert ::Logging.const_defined?('OBJ_FORMAT') 218 | assert_equal :inspect, ::Logging::OBJ_FORMAT 219 | remove_const[:OBJ_FORMAT] 220 | 221 | ::Logging.format_as :json 222 | assert ::Logging.const_defined?('OBJ_FORMAT') 223 | assert_equal :json, ::Logging::OBJ_FORMAT 224 | remove_const[:OBJ_FORMAT] 225 | 226 | ::Logging.format_as :yaml 227 | assert ::Logging.const_defined?('OBJ_FORMAT') 228 | assert_equal :yaml, ::Logging::OBJ_FORMAT 229 | remove_const[:OBJ_FORMAT] 230 | 231 | ::Logging.format_as 'string' 232 | assert ::Logging.const_defined?('OBJ_FORMAT') 233 | assert_equal :string, ::Logging::OBJ_FORMAT 234 | remove_const[:OBJ_FORMAT] 235 | 236 | ::Logging.format_as 'inspect' 237 | assert ::Logging.const_defined?('OBJ_FORMAT') 238 | assert_equal :inspect, ::Logging::OBJ_FORMAT 239 | remove_const[:OBJ_FORMAT] 240 | 241 | ::Logging.format_as 'yaml' 242 | assert ::Logging.const_defined?('OBJ_FORMAT') 243 | assert_equal :yaml, ::Logging::OBJ_FORMAT 244 | remove_const[:OBJ_FORMAT] 245 | end 246 | 247 | def test_path 248 | path = ::Logging.path(*%w[one two three]) 249 | assert_match %r/one\/two\/three$/, path 250 | end 251 | 252 | def test_version 253 | assert_match %r/\d+\.\d+\.\d+/, ::Logging.version 254 | end 255 | 256 | class Failer 257 | class WriteError < StandardError ; end 258 | def self.write(*args) 259 | raise WriteError.new("Oh noooooo") 260 | end 261 | end 262 | 263 | def test_error_handling 264 | logger = ::Logging.logger Failer, 2, 100 265 | logger.appenders.first.level = :debug 266 | 267 | # No errors are raised by default 268 | logger.fatal 'this is a debug message' 269 | # Always reset the level; we disable appenders that raise by setting them 270 | # to :off 271 | logger.appenders.first.level = :debug 272 | 273 | begin 274 | Logging.raise_errors = true 275 | assert_raises Failer::WriteError do 276 | logger.fatal 'this fails because the file descriptor is closed' 277 | end 278 | ensure 279 | Logging.raise_errors = false 280 | end 281 | end 282 | end 283 | end 284 | -------------------------------------------------------------------------------- /lib/logging/appender.rb: -------------------------------------------------------------------------------- 1 | 2 | module Logging 3 | 4 | # The +Appender+ class is provides methods for appending log events to a 5 | # logging destination. The log events are formatted into strings using a 6 | # Layout. 7 | # 8 | # All other Appenders inherit from this class which provides stub methods. 9 | # Each subclass should provide a +write+ method that will write log 10 | # messages to the logging destination. 11 | # 12 | class Appender 13 | 14 | attr_reader :name, :layout, :level, :filters 15 | 16 | # call-seq: 17 | # Appender.new( name ) 18 | # Appender.new( name, :layout => layout ) 19 | # 20 | # Creates a new appender using the given name. If no Layout is specified, 21 | # then a Basic layout will be used. Any logging header supplied by the 22 | # layout will be written to the logging destination when the Appender is 23 | # created. 24 | # 25 | # Options: 26 | # 27 | # :layout => the layout to use when formatting log events 28 | # :level => the level at which to log 29 | # :encoding => encoding to use when writing messages (defaults to UTF-8) 30 | # :filters => filters to apply to events before processing 31 | # 32 | def initialize( name, opts = {} ) 33 | ::Logging.init unless ::Logging.initialized? 34 | 35 | @name = name.to_s 36 | @closed = false 37 | @filters = [] 38 | @mutex = ReentrantMutex.new 39 | 40 | self.layout = opts.fetch(:layout, ::Logging::Layouts::Basic.new) 41 | self.level = opts.fetch(:level, nil) 42 | self.encoding = opts.fetch(:encoding, self.encoding) 43 | self.filters = opts.fetch(:filters, nil) 44 | 45 | if opts.fetch(:header, true) 46 | header = @layout.header 47 | 48 | unless header.nil? || header.empty? 49 | begin 50 | write(header) 51 | rescue StandardError => err 52 | ::Logging.log_internal_error(err) 53 | end 54 | end 55 | end 56 | 57 | ::Logging::Appenders[@name] = self 58 | end 59 | 60 | # call-seq: 61 | # append( event ) 62 | # 63 | # Write the given _event_ to the logging destination. The log event will 64 | # be processed through the Layout associated with the Appender. 65 | # 66 | def append( event ) 67 | if @closed 68 | raise RuntimeError, 69 | "appender '<#{self.class.name}: #{@name}>' is closed" 70 | end 71 | 72 | # only append if the event level is less than or equal to the configured 73 | # appender level and the filter does not disallow it 74 | if event = allow(event) 75 | begin 76 | write(event) 77 | rescue StandardError => err 78 | ::Logging.log_internal_error(err) 79 | end 80 | end 81 | 82 | self 83 | end 84 | 85 | # call-seq: 86 | # appender << string 87 | # 88 | # Write the given _string_ to the logging destination "as is" -- no 89 | # layout formatting will be performed. 90 | # 91 | def <<( str ) 92 | if @closed 93 | raise RuntimeError, 94 | "appender '<#{self.class.name}: #{@name}>' is closed" 95 | end 96 | 97 | unless off? 98 | begin 99 | write(str) 100 | rescue StandardError => err 101 | ::Logging.log_internal_error(err) 102 | end 103 | end 104 | self 105 | end 106 | 107 | # call-seq: 108 | # level = :all 109 | # 110 | # Set the level for this appender; log events below this level will be 111 | # ignored by this appender. The level can be either a +String+, a 112 | # +Symbol+, or an +Integer+. An +ArgumentError+ is raised if this is not 113 | # the case. 114 | # 115 | # There are two special levels -- "all" and "off". The former will 116 | # enable recording of all log events. The latter will disable the 117 | # recording of all events. 118 | # 119 | # Example: 120 | # 121 | # appender.level = :debug 122 | # appender.level = "INFO" 123 | # appender.level = 4 124 | # appender.level = 'off' 125 | # appender.level = :all 126 | # 127 | # These produce an +ArgumentError+ 128 | # 129 | # appender.level = Object 130 | # appender.level = -1 131 | # appender.level = 1_000_000_000_000 132 | # 133 | def level=( level ) 134 | lvl = case level 135 | when String, Symbol; ::Logging::level_num(level) 136 | when Integer; level 137 | when nil; 0 138 | else 139 | raise ArgumentError, 140 | "level must be a String, Symbol, or Integer" 141 | end 142 | if lvl.nil? or lvl < 0 or lvl > ::Logging::LEVELS.length 143 | raise ArgumentError, "unknown level was given '#{level}'" 144 | end 145 | 146 | @level = lvl 147 | end 148 | 149 | # call-seq 150 | # appender.layout = Logging::Layouts::Basic.new 151 | # 152 | # Sets the layout to be used by this appender. 153 | # 154 | def layout=( layout ) 155 | unless layout.kind_of? ::Logging::Layout 156 | raise TypeError, 157 | "#{layout.inspect} is not a kind of 'Logging::Layout'" 158 | end 159 | @layout = layout 160 | end 161 | 162 | # Sets the filter(s) to be used by this appender. This method will clear the 163 | # current filter set and add those passed to this setter method. 164 | # 165 | # Examples 166 | # appender.filters = Logging::Filters::Level.new(:warn, :error) 167 | # 168 | def filters=( args ) 169 | @filters.clear 170 | add_filters(*args) 171 | end 172 | 173 | # Sets the filter(s) to be used by this appender. The filters will be 174 | # applied in the order that they are added to the appender. 175 | # 176 | # Examples 177 | # add_filters(Logging::Filters::Level.new(:warn, :error)) 178 | # 179 | # Returns this appender instance. 180 | def add_filters( *args ) 181 | args.flatten.each do |filter| 182 | next if filter.nil? 183 | unless filter.kind_of?(::Logging::Filter) 184 | raise TypeError, "#{filter.inspect} is not a kind of 'Logging::Filter'" 185 | end 186 | @filters << filter 187 | end 188 | self 189 | end 190 | 191 | # call-seq: 192 | # close( footer = true ) 193 | # 194 | # Close the appender and writes the layout footer to the logging 195 | # destination if the _footer_ flag is set to +true+. Log events will 196 | # no longer be written to the logging destination after the appender 197 | # is closed. 198 | # 199 | def close( footer = true ) 200 | return self if @closed 201 | ::Logging::Appenders.remove(@name) 202 | @closed = true 203 | 204 | flush 205 | 206 | if footer 207 | footer = @layout.footer 208 | unless footer.nil? || footer.empty? 209 | begin 210 | write(footer) 211 | rescue StandardError => err 212 | ::Logging.log_internal_error(err) 213 | end 214 | end 215 | end 216 | self 217 | end 218 | 219 | # call-seq: 220 | # closed? 221 | # 222 | # Returns +true+ if the appender has been closed; returns +false+ 223 | # otherwise. When an appender is closed, no more log events can be 224 | # written to the logging destination. 225 | # 226 | def closed? 227 | @closed 228 | end 229 | 230 | # Reopen the connection to the underlying logging destination. If the 231 | # connection is currently closed then it will be opened. If the connection 232 | # is currently open then it will be closed and immediately opened. 233 | # 234 | def reopen 235 | @closed = false 236 | self 237 | end 238 | 239 | # call-seq: 240 | # flush 241 | # 242 | # Call +flush+ to force an appender to write out any buffered log events. 243 | # Similar to IO#flush, so use in a similar fashion. 244 | # 245 | def flush 246 | self 247 | end 248 | 249 | # Save off the original `to_s` for use in tests 250 | alias_method :_to_s, :to_s 251 | 252 | # call-seq: 253 | # to_s => string 254 | # 255 | # Returns a string representation of the appender. 256 | # 257 | def to_s 258 | "<%s name=\"%s\">" % [self.class.name.sub(%r/^Logging::/, ''), self.name] 259 | end 260 | 261 | # Returns the current Encoding for the appender. The default external econding 262 | # will be used if none is explicitly set. 263 | attr_reader :encoding 264 | 265 | # Set the appender encoding to the given value. The value can either be an 266 | # Encoding instance or a String or Symbol referring to a valid encoding. 267 | # 268 | # This method only applies to Ruby 1.9 or later. The encoding will always be 269 | # nil for older Rubies. 270 | # 271 | # value - The encoding as a String, Symbol, or Encoding instance. 272 | # 273 | # Raises ArgumentError if the value is not a valid encoding. 274 | def encoding=( value ) 275 | if value.nil? 276 | @encoding = Encoding.default_external 277 | else 278 | @encoding = Encoding.find(value.to_s) 279 | end 280 | end 281 | 282 | # Check to see if the event should be processed by the appender. An event will 283 | # be rejected if the event level is lower than the configured level for the 284 | # appender. Or it will be rejected if one of the filters rejects the event. 285 | # 286 | # event - The LogEvent to check 287 | # 288 | # Returns the event if it is allowed; returns `nil` if it is not allowed. 289 | def allow( event ) 290 | return nil if @level > event.level 291 | @filters.each do |filter| 292 | break unless event = filter.allow(event) 293 | end 294 | event 295 | end 296 | 297 | # Returns `true` if the appender has been turned off. This is useful for 298 | # appenders that write data to a remote location (such as syslog or email), 299 | # and that write encounters too many errors. The appender can turn itself off 300 | # to and log an error via the `Logging` logger. 301 | # 302 | # Set the appender's level to a valid value to turn it back on. 303 | def off? 304 | @level >= ::Logging::LEVELS.length 305 | end 306 | 307 | 308 | private 309 | 310 | # call-seq: 311 | # write( event ) 312 | # 313 | # Writes the given _event_ to the logging destination. Subclasses should 314 | # provide an implementation of this method. The _event_ can be either a 315 | # LogEvent or a String. If a LogEvent, then it will be formatted using 316 | # the layout given to the appender when it was created. 317 | # 318 | def write( event ) 319 | nil 320 | end 321 | 322 | end # class Appender 323 | end # module Logging 324 | 325 | --------------------------------------------------------------------------------