├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── chrono_logger.gemspec ├── lib ├── chrono_logger.rb └── chrono_logger │ └── version.rb └── test ├── from_ruby_repo ├── test_logdevice.rb ├── test_logger.rb └── test_severity.rb ├── helper.rb └── test_chrono_logger.rb /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Ruby 9 | 10 | on: 11 | push: 12 | branches: [ master ] 13 | pull_request: 14 | 15 | jobs: 16 | test: 17 | 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | ruby: [ '2.6', '2.7', '3.0', '3.1' ] 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Set up Ruby 26 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, 27 | # change this to (see https://github.com/ruby/setup-ruby#versioning): 28 | uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{ matrix.ruby }} 31 | - name: Install dependencies 32 | run: | 33 | bundle config path vendor/bundle 34 | bundle install --jobs 4 --retry 3 35 | - name: Run tests 36 | run: bundle exec rake 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | /vendor/bundle 16 | .ruby-version 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.1.0 (2015-08-17) 4 | 5 | * Enhancements 6 | * strftime format for directory name 7 | 8 | ```ruby 9 | logger = ChronoLogger.new('%Y/%m/%d.log') 10 | ``` 11 | 12 | ## 1.0.0 (2015-02-05) 13 | 14 | * First Major Release 15 | 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in chrono_logger.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Takayuki Matsubara 2 | 3 | MIT License 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 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChronoLogger 2 | 3 | ## :warning: This library is no longer maintained :warning: 4 | 5 | A lock-free logger with timebased file rotation. 6 | 7 | Ruby's stdlib `Logger` wraps `IO#write` in mutexes. `ChronoLogger` removes these mutexes. 8 | 9 | `ChronoLogger` provides time based file rotation such as: 10 | 11 | ``` 12 | logger = ChronoLogger.new('/log/production.log.%Y%m%d') 13 | Time.now.strftime('%F') 14 | # => "2015-01-26" 15 | File.exist?('/log/production.log.20150126') 16 | # => true 17 | 18 | # one day later 19 | Time.now.strftime('%F') 20 | # => "2015-01-27" 21 | logger.write('hi next day') 22 | File.exist?('/log/production.log.20150127') 23 | # => true 24 | ``` 25 | 26 | ## Motivation 27 | 28 | Current my projects uses `::Logger` with cronolog. So 29 | 30 | - Reduce dependency such as cronolog 31 | - Remove mutexes in ruby world because os already does when some environments (ex: ext4 file system) 32 | - Support time based rotation without renaming file because file renaming sometime makes problem 33 | 34 | ## Installation 35 | 36 | Add this line to your application's Gemfile: 37 | 38 | ```ruby 39 | gem 'chrono_logger' 40 | ``` 41 | 42 | And then execute: 43 | 44 | $ bundle 45 | 46 | Or install it yourself as: 47 | 48 | $ gem install chrono_logger 49 | 50 | ## Usage 51 | 52 | Same interfaces ruby's stdlib `Logger` except for `new` method. 53 | 54 | ```ruby 55 | require 'chrono_logger' 56 | 57 | # specify path with `Time#strftime` format 58 | logger = ChronoLogger.new('development.%Y%m%d') 59 | 60 | logger.error("Enjoy") 61 | logger.warn("logging!") 62 | logger.info("Enjoy") 63 | logger.debug("programming!") 64 | ``` 65 | 66 | With Rails: 67 | 68 | ```ruby 69 | # in config/environments/{development,production}.rb 70 | 71 | config.logger = ChronoLogger.new("#{config.paths['log'].first}.%Y%m%d") 72 | ``` 73 | 74 | ## Migrating from `::Logger` with cronolog 75 | 76 | You only change `Logger.new` into `ChronoLogger.new`: 77 | 78 | ```ruby 79 | # for instance your setup is like the following 80 | Logger.new(IO.popen("/usr/sbin/cronolog production.%Y%m%d", "w")) 81 | 82 | # turns into 83 | ChronoLogger.new('production.%Y%m%d') 84 | ``` 85 | 86 | ## Limitation 87 | 88 | - High performance logging only daily based time formatting path for example `'%Y%m%d'`. You can create pull request if you need other time period. 89 | 90 | ## Contributing 91 | 92 | 1. Fork it ( https://github.com/ma2gedev/chrono_logger/fork ) 93 | 2. Create your feature branch (`git checkout -b my-new-feature`) 94 | 3. Commit your changes (`git commit -am 'Add some feature'`) 95 | 4. Push to the branch (`git push origin my-new-feature`) 96 | 5. Create a new Pull Request 97 | 98 | ## License 99 | 100 | MIT. See [LICENSE.txt](LICENSE.txt) for more details. 101 | 102 | ## Resources 103 | 104 | - [ChronoLogger logging is 1.5x faster than ruby's stdlib Logger](https://coderwall.com/p/vjjszq/chronologger-logging-is-1-5x-faster-than-ruby-s-stdlib-logger) 105 | 106 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.test_files = Dir["test/**/test_*.rb"] 7 | end 8 | 9 | task :default => :test 10 | 11 | -------------------------------------------------------------------------------- /chrono_logger.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'chrono_logger/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "chrono_logger" 8 | spec.version = ChronoLogger::VERSION 9 | spec.authors = ["Takayuki Matsubara"] 10 | spec.email = ["takayuki.1229@gmail.com"] 11 | spec.summary = %q{A lock-free logger with timebased file rotation.} 12 | spec.description = %q{A lock-free logger with timebased file rotation.} 13 | spec.homepage = "https://github.com/ma2gedev/chrono_logger" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "rake", ">= 0" 22 | spec.add_development_dependency "test-unit" 23 | spec.add_development_dependency "pry" 24 | spec.add_development_dependency "delorean" 25 | spec.add_development_dependency "parallel" 26 | spec.add_development_dependency "simplecov" 27 | 28 | # for performance check 29 | spec.add_development_dependency "mono_logger" 30 | spec.add_development_dependency "log4r" 31 | end 32 | -------------------------------------------------------------------------------- /lib/chrono_logger.rb: -------------------------------------------------------------------------------- 1 | require "chrono_logger/version" 2 | require 'logger' 3 | require 'pathname' 4 | 5 | # A lock-free logger with timebased file rotation. 6 | class ChronoLogger < Logger 7 | 8 | # @param logdev [String, IO] `Time#strftime` formatted filename (String) or IO object (typically STDOUT, STDERR, or an open file). 9 | # @example 10 | # 11 | # ChronoLogger.new('/log/production.log.%Y%m%d') 12 | # Time.now.strftime('%F') => "2015-01-29" 13 | # File.exist?('/log/production.log.20150129') => true 14 | # 15 | def initialize(logdev) 16 | @progname = nil 17 | @level = DEBUG 18 | @default_formatter = ::Logger::Formatter.new 19 | @formatter = nil 20 | @logdev = nil 21 | if logdev 22 | @logdev = TimeBasedLogDevice.new(logdev) 23 | end 24 | end 25 | 26 | module Period 27 | DAILY = 1 28 | 29 | SiD = 24 * 60 * 60 30 | 31 | def determine_period(format) 32 | case format 33 | when /%[SscXrT]/ then nil # seconds 34 | when /%[MR]/ then nil # minutes 35 | when /%[HklI]/ then nil # hours 36 | when /%[dejDFvx]/ then DAILY 37 | else nil 38 | end 39 | end 40 | 41 | def next_start_period(now, period) 42 | case period 43 | when DAILY 44 | Time.mktime(now.year, now.month, now.mday) + SiD 45 | else 46 | nil 47 | end 48 | end 49 | end 50 | 51 | class TimeBasedLogDevice < LogDevice 52 | include Period 53 | 54 | DELAY_SECOND_TO_CLOSE_FILE = 5 55 | 56 | def initialize(log = nil, opt = {}) 57 | @dev = @filename = @pattern = nil 58 | if defined?(LogDeviceMutex) # Ruby < 2.3 59 | @mutex = LogDeviceMutex.new 60 | else 61 | mon_initialize 62 | @mutex = self 63 | end 64 | if log.respond_to?(:write) and log.respond_to?(:close) 65 | @dev = log 66 | else 67 | @pattern = log 68 | @period = determine_period(@pattern) 69 | now = Time.now 70 | @filename = now.strftime(@pattern) 71 | @next_start_period = next_start_period(now, @period) 72 | @dev = open_logfile(@filename) 73 | @dev.sync = true 74 | end 75 | end 76 | 77 | def write(message) 78 | check_and_shift_log if @pattern 79 | @dev.write(message) 80 | rescue 81 | warn("log writing failed. #{$!}") 82 | end 83 | 84 | def close 85 | @dev.close rescue nil 86 | end 87 | 88 | private 89 | 90 | def open_logfile(filename) 91 | begin 92 | open(filename, (File::WRONLY | File::APPEND)) 93 | rescue Errno::ENOENT 94 | create_logfile(filename) 95 | end 96 | end 97 | 98 | def create_logfile(filename) 99 | begin 100 | Pathname(filename).dirname.mkpath 101 | logdev = open(filename, (File::WRONLY | File::APPEND | File::CREAT | File::EXCL)) 102 | logdev.sync = true 103 | rescue Errno::EEXIST 104 | # file is created by another process 105 | logdev = open_logfile(filename) 106 | logdev.sync = true 107 | end 108 | logdev 109 | end 110 | 111 | def check_and_shift_log 112 | if next_period?(Time.now) 113 | now = Time.now 114 | new_filename = now.strftime(@pattern) 115 | next_start_period = next_start_period(now, @period) 116 | shift_log(new_filename) 117 | @filename = new_filename 118 | @next_start_period = next_start_period 119 | end 120 | end 121 | 122 | def next_period?(now) 123 | if @period 124 | @next_start_period <= now 125 | else 126 | Time.now.strftime(@pattern) != @filename 127 | end 128 | end 129 | 130 | def shift_log(filename) 131 | begin 132 | @mutex.synchronize do 133 | tmp_dev = @dev 134 | @dev = create_logfile(filename) 135 | Thread.new(tmp_dev) do |tmp_dev| 136 | sleep DELAY_SECOND_TO_CLOSE_FILE 137 | tmp_dev.close rescue nil 138 | end 139 | end 140 | rescue Exception => ignored 141 | warn("log shifting failed. #{ignored}") 142 | end 143 | end 144 | end 145 | 146 | # EXPERIMENTAL: this formatter faster than default `Logger::Formatter` 147 | class Formatter < ::Logger::Formatter 148 | DATETIME_SPRINTF_FORMAT = "%04d-%02d-%02dT%02d:%02d:%02d.%06d ".freeze 149 | 150 | # same as `Logger::Formatter#format_datetime`'s default behaviour 151 | def format_datetime(t) 152 | DATETIME_SPRINTF_FORMAT % [t.year, t.month, t.day, t.hour, t.min, t.sec, t.tv_usec] 153 | end 154 | 155 | def datetime_format=(datetime_format) 156 | raise 'do not support' 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /lib/chrono_logger/version.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | class ChronoLogger < Logger 4 | VERSION = "1.1.2" 5 | end 6 | -------------------------------------------------------------------------------- /test/from_ruby_repo/test_logdevice.rb: -------------------------------------------------------------------------------- 1 | # coding: US-ASCII 2 | require 'helper' 3 | require 'tempfile' 4 | require 'tmpdir' 5 | require 'parallel' 6 | require 'delorean' 7 | require 'pry' 8 | 9 | class TestLogDevice < Test::Unit::TestCase 10 | class LogExcnRaiser 11 | def write(*arg) 12 | raise 'disk is full' 13 | end 14 | 15 | def close 16 | end 17 | end 18 | 19 | def setup 20 | @tempfile = Tempfile.new("logger") 21 | @tempfile.close 22 | @format = [@tempfile.path, '%Y%m%d'].join 23 | @filename = Time.now.strftime(@format) 24 | File.unlink(@tempfile.path) 25 | end 26 | 27 | def teardown 28 | @tempfile.close(true) 29 | end 30 | 31 | def d(format, opt = {}) 32 | ChronoLogger::TimeBasedLogDevice.new(format, opt) 33 | end 34 | 35 | def test_initialize 36 | logdev = d(STDERR) 37 | assert_equal(STDERR, logdev.dev) 38 | assert_nil(logdev.filename) 39 | assert_raises(TypeError) do 40 | d(nil) 41 | end 42 | # 43 | logdev = d(@format) 44 | begin 45 | assert(File.exist?(@filename)) 46 | assert(logdev.dev.sync) 47 | assert_equal(@filename, logdev.filename) 48 | logdev.write('hello') 49 | ensure 50 | logdev.close 51 | end 52 | # create logfile whitch is already exist. 53 | logdev = d(@format) 54 | begin 55 | logdev.write('world') 56 | logfile = File.read(@filename) 57 | assert_equal(1, logfile.split(/\n/).size) 58 | assert_match(/^helloworld$/, logfile) 59 | ensure 60 | logdev.close 61 | end 62 | end 63 | 64 | def test_write 65 | r, w = IO.pipe 66 | logdev = d(w) 67 | logdev.write("msg2\n\n") 68 | IO.select([r], nil, nil, 0.1) 69 | w.close 70 | msg = r.read 71 | r.close 72 | assert_equal("msg2\n\n", msg) 73 | # 74 | logdev = d(LogExcnRaiser.new) 75 | class << (stderr = '') 76 | alias write << 77 | end 78 | $stderr, stderr = stderr, $stderr 79 | begin 80 | assert_nothing_raised do 81 | logdev.write('hello') 82 | end 83 | ensure 84 | logdev.close 85 | $stderr, stderr = stderr, $stderr 86 | end 87 | assert_equal "log writing failed. disk is full\n", stderr 88 | end 89 | 90 | def test_close 91 | r, w = IO.pipe 92 | logdev = d(w) 93 | logdev.write("msg2\n\n") 94 | IO.select([r], nil, nil, 0.1) 95 | assert(!w.closed?) 96 | logdev.close 97 | assert(w.closed?) 98 | r.close 99 | end 100 | 101 | def test_shifting_midnight 102 | Dir.mktmpdir do |tmpdir| 103 | log = "log20140102" 104 | old_log = File.join(tmpdir, log) 105 | begin 106 | File.open(old_log, "w") {} 107 | File.utime(*[Time.mktime(2014, 1, 1, 23, 59, 59)]*2, old_log) 108 | 109 | Delorean.time_travel_to '2014-01-02 23:59:59' 110 | dev = ChronoLogger::TimeBasedLogDevice.new(File.join(tmpdir, "log%Y%m%d")) 111 | dev.write("#{Time.now} hello-1\n") 112 | 113 | Delorean.time_travel_to '2014-01-03 00:00:01' 114 | dev.write("#{Time.now} hello-2\n") 115 | ensure 116 | Delorean.back_to_the_present 117 | dev.close 118 | end 119 | 120 | cont = File.read(old_log) 121 | assert_match(/hello-1/, cont) 122 | assert_not_match(/hello-2/, cont) 123 | new_log = File.join(tmpdir, "log20140103") 124 | bug = '[GH-539]' 125 | assert { File.exist?(new_log) } 126 | assert_match(/hello-2/, File.read(new_log), bug) 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /test/from_ruby_repo/test_logger.rb: -------------------------------------------------------------------------------- 1 | # coding: US-ASCII 2 | require 'helper' 3 | require 'tempfile' 4 | 5 | class TestLogger < Test::Unit::TestCase 6 | include ChronoLogger::Severity 7 | 8 | def setup 9 | @logger = ChronoLogger.new(nil) 10 | end 11 | 12 | class Log 13 | attr_reader :label, :datetime, :pid, :severity, :progname, :msg 14 | def initialize(line) 15 | # NOTE: adapt the following pattern ruby's original test after support only ruby 3.1 or higher 16 | /\A(\w+), \[([^#]*)#(\d+)\]\s+(\w+) -- (\w*): ([\x0-\xff]*)/ =~ line 17 | @label, @datetime, @pid, @severity, @progname, @msg = $1, $2, $3, $4, $5, $6 18 | end 19 | end 20 | 21 | def log_add(logger, severity, msg, progname = nil, &block) 22 | log(logger, :add, severity, msg, progname, &block) 23 | end 24 | 25 | def log(logger, msg_id, *arg, &block) 26 | Log.new(log_raw(logger, msg_id, *arg, &block)) 27 | end 28 | 29 | def log_raw(logger, msg_id, *arg, &block) 30 | logdev = Tempfile.new(File.basename(__FILE__) + '.log') 31 | logger.instance_eval { @logdev = ChronoLogger::TimeBasedLogDevice.new(logdev) } 32 | logger.__send__(msg_id, *arg, &block) 33 | logdev.open 34 | msg = logdev.read 35 | logdev.close(true) 36 | msg 37 | end 38 | 39 | def test_level 40 | @logger.level = UNKNOWN 41 | assert_equal(UNKNOWN, @logger.level) 42 | @logger.level = INFO 43 | assert_equal(INFO, @logger.level) 44 | @logger.sev_threshold = ERROR 45 | assert_equal(ERROR, @logger.sev_threshold) 46 | @logger.sev_threshold = WARN 47 | assert_equal(WARN, @logger.sev_threshold) 48 | assert_equal(WARN, @logger.level) 49 | 50 | @logger.level = DEBUG 51 | assert(@logger.debug?) 52 | assert(@logger.info?) 53 | @logger.level = INFO 54 | assert(!@logger.debug?) 55 | assert(@logger.info?) 56 | assert(@logger.warn?) 57 | @logger.level = WARN 58 | assert(!@logger.info?) 59 | assert(@logger.warn?) 60 | assert(@logger.error?) 61 | @logger.level = ERROR 62 | assert(!@logger.warn?) 63 | assert(@logger.error?) 64 | assert(@logger.fatal?) 65 | @logger.level = FATAL 66 | assert(!@logger.error?) 67 | assert(@logger.fatal?) 68 | @logger.level = UNKNOWN 69 | assert(!@logger.error?) 70 | assert(!@logger.fatal?) 71 | end 72 | 73 | def test_progname 74 | assert_nil(@logger.progname) 75 | @logger.progname = "name" 76 | assert_equal("name", @logger.progname) 77 | end 78 | 79 | def test_datetime_format 80 | dummy = STDERR 81 | logger = ChronoLogger.new(dummy) 82 | log = log_add(logger, INFO, "foo") 83 | assert_match(/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d.\s*\d+ $/, log.datetime) 84 | logger.datetime_format = "%d%b%Y@%H:%M:%S" 85 | log = log_add(logger, INFO, "foo") 86 | # NOTE: ` ?` contained in the following will be removed after support only ruby 3.1 or higher 87 | assert_match(/^\d\d\w\w\w\d\d\d\d@\d\d:\d\d:\d\d ?$/, log.datetime) 88 | logger.datetime_format = "" 89 | log = log_add(logger, INFO, "foo") 90 | assert_match(/^ ?$/, log.datetime) 91 | end 92 | 93 | def test_formatter 94 | dummy = STDERR 95 | logger = ChronoLogger.new(dummy) 96 | # default 97 | log = log(logger, :info, "foo") 98 | assert_equal("foo\n", log.msg) 99 | # config 100 | logger.formatter = proc { |severity, timestamp, progname, msg| 101 | "#{severity}:#{msg}\n\n" 102 | } 103 | line = log_raw(logger, :info, "foo") 104 | assert_equal("INFO:foo\n\n", line) 105 | # recover 106 | logger.formatter = nil 107 | log = log(logger, :info, "foo") 108 | assert_equal("foo\n", log.msg) 109 | # again 110 | o = Object.new 111 | def o.call(severity, timestamp, progname, msg) 112 | "<<#{severity}-#{msg}>>\n" 113 | end 114 | logger.formatter = o 115 | line = log_raw(logger, :info, "foo") 116 | assert_equal("<"">\n", line) 117 | end 118 | 119 | def test_initialize 120 | logger = ChronoLogger.new(STDERR) 121 | assert_nil(logger.progname) 122 | assert_equal(DEBUG, logger.level) 123 | assert_nil(logger.datetime_format) 124 | end 125 | 126 | def test_add 127 | logger = ChronoLogger.new(nil) 128 | logger.progname = "my_progname" 129 | assert(logger.add(INFO)) 130 | log = log_add(logger, nil, "msg") 131 | assert_equal("ANY", log.severity) 132 | assert_equal("my_progname", log.progname) 133 | logger.level = WARN 134 | assert(logger.log(INFO)) 135 | assert_nil(log_add(logger, INFO, "msg").msg) 136 | log = log_add(logger, WARN, nil) { "msg" } 137 | assert_equal("msg\n", log.msg) 138 | log = log_add(logger, WARN, "") { "msg" } 139 | assert_equal("\n", log.msg) 140 | assert_equal("my_progname", log.progname) 141 | log = log_add(logger, WARN, nil, "progname?") 142 | assert_equal("progname?\n", log.msg) 143 | assert_equal("my_progname", log.progname) 144 | end 145 | 146 | def test_level_log 147 | logger = ChronoLogger.new(nil) 148 | logger.progname = "my_progname" 149 | log = log(logger, :debug, "custom_progname") { "msg" } 150 | assert_equal("msg\n", log.msg) 151 | assert_equal("custom_progname", log.progname) 152 | assert_equal("DEBUG", log.severity) 153 | assert_equal("D", log.label) 154 | # 155 | log = log(logger, :debug) { "msg_block" } 156 | assert_equal("msg_block\n", log.msg) 157 | assert_equal("my_progname", log.progname) 158 | log = log(logger, :debug, "msg_inline") 159 | assert_equal("msg_inline\n", log.msg) 160 | assert_equal("my_progname", log.progname) 161 | # 162 | log = log(logger, :info, "custom_progname") { "msg" } 163 | assert_equal("msg\n", log.msg) 164 | assert_equal("custom_progname", log.progname) 165 | assert_equal("INFO", log.severity) 166 | assert_equal("I", log.label) 167 | # 168 | log = log(logger, :warn, "custom_progname") { "msg" } 169 | assert_equal("msg\n", log.msg) 170 | assert_equal("custom_progname", log.progname) 171 | assert_equal("WARN", log.severity) 172 | assert_equal("W", log.label) 173 | # 174 | log = log(logger, :error, "custom_progname") { "msg" } 175 | assert_equal("msg\n", log.msg) 176 | assert_equal("custom_progname", log.progname) 177 | assert_equal("ERROR", log.severity) 178 | assert_equal("E", log.label) 179 | # 180 | log = log(logger, :fatal, "custom_progname") { "msg" } 181 | assert_equal("msg\n", log.msg) 182 | assert_equal("custom_progname", log.progname) 183 | assert_equal("FATAL", log.severity) 184 | assert_equal("F", log.label) 185 | # 186 | log = log(logger, :unknown, "custom_progname") { "msg" } 187 | assert_equal("msg\n", log.msg) 188 | assert_equal("custom_progname", log.progname) 189 | assert_equal("ANY", log.severity) 190 | assert_equal("A", log.label) 191 | end 192 | 193 | def test_close 194 | r, w = IO.pipe 195 | assert(!w.closed?) 196 | logger = ChronoLogger.new(w) 197 | logger.close 198 | assert(w.closed?) 199 | r.close 200 | end 201 | 202 | class MyError < StandardError 203 | end 204 | 205 | class MyMsg 206 | def inspect 207 | "my_msg" 208 | end 209 | end 210 | 211 | def test_format 212 | logger = ChronoLogger.new(nil) 213 | log = log_add(logger, INFO, "msg\n") 214 | assert_equal("msg\n\n", log.msg) 215 | begin 216 | raise MyError.new("excn") 217 | rescue MyError => e 218 | log = log_add(logger, INFO, e) 219 | assert_match(/^excn \(TestLogger::MyError\)/, log.msg) 220 | # expects backtrace is dumped across multi lines. 10 might be changed. 221 | assert(log.msg.split(/\n/).size >= 10) 222 | end 223 | log = log_add(logger, INFO, MyMsg.new) 224 | assert_equal("my_msg\n", log.msg) 225 | end 226 | 227 | def test_lshift 228 | r, w = IO.pipe 229 | logger = ChronoLogger.new(w) 230 | logger << "msg" 231 | read_ready, = IO.select([r], nil, nil, 0.1) 232 | w.close 233 | msg = r.read 234 | r.close 235 | assert_equal("msg", msg) 236 | # 237 | r, w = IO.pipe 238 | logger = ChronoLogger.new(w) 239 | logger << "msg2\n\n" 240 | read_ready, = IO.select([r], nil, nil, 0.1) 241 | w.close 242 | msg = r.read 243 | r.close 244 | assert_equal("msg2\n\n", msg) 245 | end 246 | end 247 | -------------------------------------------------------------------------------- /test/from_ruby_repo/test_severity.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestLoggerSeverity < Test::Unit::TestCase 4 | def test_enum 5 | logger_levels = ChronoLogger.constants 6 | levels = ["WARN", "UNKNOWN", "INFO", "FATAL", "DEBUG", "ERROR"] 7 | ChronoLogger::Severity.constants.each do |level| 8 | assert(levels.include?(level.to_s)) 9 | assert(logger_levels.include?(level)) 10 | end 11 | assert_equal(levels.size, ChronoLogger::Severity.constants.size) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | 4 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 5 | require 'chrono_logger' 6 | 7 | require 'test/unit' 8 | -------------------------------------------------------------------------------- /test/test_chrono_logger.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | require 'tempfile' 3 | require 'tmpdir' 4 | require 'parallel' 5 | require 'delorean' 6 | 7 | class TestChronoLogger < Test::Unit::TestCase 8 | def setup 9 | @tempfile = Tempfile.new("logger") 10 | @tempfile.close 11 | @format = [@tempfile.path, '%Y%m%d'].join 12 | @filename = Time.now.strftime(@format) 13 | File.unlink(@tempfile.path) 14 | end 15 | 16 | def teardown 17 | @tempfile.close(true) 18 | end 19 | 20 | def test_that_it_has_a_version_number 21 | refute_nil ::ChronoLogger::VERSION 22 | end 23 | 24 | def test_shifting_age_in_multithreads 25 | confirm_daily_rotation(in_threads: 2) 26 | end 27 | 28 | def test_shifting_age_in_multiprocess 29 | confirm_daily_rotation(in_processes: 2) 30 | end 31 | 32 | def test_exception_on_shifting_log 33 | old_log = [@tempfile.path, '20220101'].join 34 | new_log = [@tempfile.path, '20220102'].join 35 | $stderr, stderr = StringIO.new, $stderr 36 | begin 37 | Delorean.time_travel_to '2022-01-01 23:59:59.990' 38 | logger = ChronoLogger.new(@format) 39 | old_logdev = logger.instance_variable_get('@logdev') 40 | def old_logdev.create_logfile(filename) 41 | raise 'override create_logfile method' 42 | end 43 | sleep 1 # waiting for shift log file 44 | 45 | logger.info 'shift log' 46 | 47 | assert_match(/log shifting failed\. override create_logfile method/, $stderr.string) 48 | assert { File.exist?(old_log) } 49 | assert { !File.exist?(new_log) } 50 | ensure 51 | $stderr, stderr = stderr, $stderr 52 | Delorean.back_to_the_present 53 | File.unlink(old_log) 54 | end 55 | end 56 | 57 | def test_rotation_per_second 58 | Dir.mktmpdir do |tmpdir| 59 | begin 60 | logger = ChronoLogger.new([tmpdir, '%Y%m%dT%H%M%S'].join) 61 | Delorean.time_travel_to '2014-01-01 23:59:50' 62 | logger.debug 'rotation' 63 | Delorean.time_travel_to '2014-01-01 23:59:51' 64 | logger.debug 'per second' 65 | 66 | assert { File.exist?([tmpdir, '20140101T235950'].join) } 67 | assert { File.exist?([tmpdir, '20140101T235951'].join) } 68 | ensure 69 | Delorean.back_to_the_present 70 | end 71 | end 72 | end 73 | 74 | def test_rotation_per_day_and_create_dir 75 | Dir.mktmpdir do |tmpdir| 76 | begin 77 | Delorean.time_travel_to '2015-08-01 23:59:50' 78 | logger = ChronoLogger.new([tmpdir, '/%Y/%m/%d/test.log'].join) 79 | logger.debug 'rotation' 80 | Delorean.time_travel_to '2015-08-02 00:00:01' 81 | logger.debug 'new dir' 82 | 83 | assert { File.exist?([tmpdir, '/2015/08/01/test.log'].join) } 84 | assert { File.exist?([tmpdir, '/2015/08/02/test.log'].join) } 85 | ensure 86 | Delorean.back_to_the_present 87 | end 88 | end 89 | end 90 | 91 | class PeriodTest 92 | include ChronoLogger::Period 93 | end 94 | 95 | def test_period 96 | period = PeriodTest.new 97 | # seconds not supported 98 | assert { period.determine_period('%d%S').nil? } 99 | assert { period.determine_period('%e%s').nil? } 100 | assert { period.determine_period('%j%c').nil? } 101 | assert { period.determine_period('%j%r').nil? } 102 | assert { period.determine_period('%j%X').nil? } 103 | assert { period.determine_period('%j%T').nil? } 104 | 105 | # minutes not supported 106 | assert { period.determine_period('%d%M').nil? } 107 | assert { period.determine_period('%e%R').nil? } 108 | 109 | # hours not supported 110 | assert { period.determine_period('%d%H').nil? } 111 | assert { period.determine_period('%e%k').nil? } 112 | assert { period.determine_period('%e%l').nil? } 113 | assert { period.determine_period('%e%I').nil? } 114 | 115 | # days 116 | assert { period.determine_period('%Y%m%d') == ChronoLogger::Period::DAILY } 117 | assert { period.determine_period('%Y%m%e') == ChronoLogger::Period::DAILY } 118 | assert { period.determine_period('%Y%j') == ChronoLogger::Period::DAILY } 119 | assert { period.determine_period('%D') == ChronoLogger::Period::DAILY } 120 | assert { period.determine_period('%F') == ChronoLogger::Period::DAILY } 121 | assert { period.determine_period('%v') == ChronoLogger::Period::DAILY } 122 | assert { period.determine_period('%x') == ChronoLogger::Period::DAILY } 123 | end 124 | 125 | private 126 | 127 | def confirm_daily_rotation(option) 128 | old_log = [@tempfile.path, '20150122'].join 129 | new_log = [@tempfile.path, '20150123'].join 130 | $stderr, stderr = StringIO.new, $stderr 131 | begin 132 | Delorean.time_travel_to '2015-01-22 23:59:59.990' 133 | logger = ChronoLogger.new(@format) 134 | old_logdev = logger.instance_variable_get('@logdev').dev 135 | Parallel.map(['a', 'b'], option) do |letter| 136 | 5000.times do 137 | logger.info letter * 5000 138 | end 139 | end 140 | assert_no_match(/log shifting failed/, $stderr.string) 141 | assert_no_match(/log writing failed/, $stderr.string) 142 | assert { !old_logdev.closed? } 143 | assert { File.exist?(old_log) } 144 | assert { File.exist?(new_log) } 145 | assert { File.read(old_log).count("\n") + File.read(new_log).count("\n") == 10000 } 146 | 147 | sleep ChronoLogger::TimeBasedLogDevice::DELAY_SECOND_TO_CLOSE_FILE + 1 148 | old_logdev.close if option[:in_processes] 149 | assert { old_logdev.closed? } # TODO: check in multiprocess 150 | assert { File.exist?(old_log) } 151 | assert { File.exist?(new_log) } 152 | assert { File.read(old_log).count("\n") + File.read(new_log).count("\n") == 10000 } 153 | ensure 154 | $stderr, stderr = stderr, $stderr 155 | Delorean.back_to_the_present 156 | File.unlink(old_log) 157 | File.unlink(new_log) 158 | end 159 | end 160 | end 161 | --------------------------------------------------------------------------------