├── .document ├── .rdoc_options ├── lib ├── logger │ ├── version.rb │ ├── errors.rb │ ├── formatter.rb │ ├── severity.rb │ ├── period.rb │ └── log_device.rb └── logger.rb ├── .github ├── dependabot.yml └── workflows │ ├── test.yml │ └── push_gem.yml ├── .gitignore ├── bin ├── setup └── console ├── test ├── lib │ └── helper.rb └── logger │ ├── test_formatter.rb │ ├── test_logperiod.rb │ ├── test_severity.rb │ ├── test_logger.rb │ └── test_logdevice.rb ├── Gemfile ├── Rakefile ├── logger.gemspec ├── BSDL ├── README.md └── COPYING /.document: -------------------------------------------------------------------------------- 1 | BSDL 2 | COPYING 3 | README.md 4 | lib/ 5 | -------------------------------------------------------------------------------- /.rdoc_options: -------------------------------------------------------------------------------- 1 | --- 2 | main_page: README.md 3 | title: Documentation for Logger 4 | -------------------------------------------------------------------------------- /lib/logger/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Logger 4 | VERSION = "1.7.0" 5 | end 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | /html/ 11 | /vendor/ 12 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | # for standalone test suite on ruby/logger 3 | require 'core_assertions' 4 | 5 | Test::Unit::TestCase.include Test::Unit::CoreAssertions 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "bundler" 7 | gem "rake" 8 | gem "test-unit" 9 | gem "test-unit-ruby-core" 10 | end 11 | -------------------------------------------------------------------------------- /lib/logger/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Logger 4 | # not used after 1.2.7. just for compat. 5 | class Error < RuntimeError # :nodoc: 6 | end 7 | class ShiftingError < Error # :nodoc: 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "logger" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require "bundler/gem_tasks" 3 | rescue LoadError 4 | end 5 | 6 | require "rake/testtask" 7 | Rake::TestTask.new(:test) do |t| 8 | t.libs << "test/lib" 9 | t.ruby_opts << "-rhelper" 10 | t.test_files = FileList["test/**/test_*.rb"] 11 | end 12 | 13 | require "rdoc/task" 14 | RDoc::Task.new do |doc| 15 | doc.main = "README.md" 16 | doc.title = "Logger -- Ruby Standard Logger" 17 | doc.rdoc_files = FileList.new %w[README.md lib BSDL COPYING] 18 | doc.rdoc_dir = "html" 19 | end 20 | 21 | task "gh-pages" => :rdoc do 22 | %x[git checkout gh-pages] 23 | require "fileutils" 24 | FileUtils.rm_rf "/tmp/html" 25 | FileUtils.mv "html", "/tmp" 26 | FileUtils.rm_rf "*" 27 | FileUtils.cp_r Dir.glob("/tmp/html/*"), "." 28 | end 29 | 30 | task :default => :test 31 | -------------------------------------------------------------------------------- /lib/logger/formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Logger 4 | # Default formatter for log messages. 5 | class Formatter 6 | Format = "%.1s, [%s #%d] %5s -- %s: %s\n" 7 | DatetimeFormat = "%Y-%m-%dT%H:%M:%S.%6N" 8 | 9 | attr_accessor :datetime_format 10 | 11 | def initialize 12 | @datetime_format = nil 13 | end 14 | 15 | def call(severity, time, progname, msg) 16 | sprintf(Format, severity, format_datetime(time), Process.pid, severity, progname, msg2str(msg)) 17 | end 18 | 19 | private 20 | 21 | def format_datetime(time) 22 | time.strftime(@datetime_format || DatetimeFormat) 23 | end 24 | 25 | def msg2str(msg) 26 | case msg 27 | when ::String 28 | msg 29 | when ::Exception 30 | "#{ msg.message } (#{ msg.class })\n#{ msg.backtrace.join("\n") if msg.backtrace }" 31 | else 32 | msg.inspect 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/logger/test_formatter.rb: -------------------------------------------------------------------------------- 1 | # coding: US-ASCII 2 | # frozen_string_literal: false 3 | require 'logger' 4 | 5 | class TestFormatter < Test::Unit::TestCase 6 | def test_call 7 | severity = 'INFO' 8 | time = Time.now 9 | progname = 'ruby' 10 | msg = 'This is a test' 11 | formatter = Logger::Formatter.new 12 | 13 | result = formatter.call(severity, time, progname, msg) 14 | time_matcher = /\d{4}\-\d{2}\-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}/ 15 | matcher = /#{severity[0..0]}, \[#{time_matcher} #\d+\] #{severity} -- #{progname}: #{msg}\n/ 16 | 17 | assert_match(matcher, result) 18 | end 19 | 20 | class CustomFormatter < Logger::Formatter 21 | def call(time) 22 | format_datetime(time) 23 | end 24 | end 25 | 26 | def test_format_datetime 27 | time = Time.now 28 | formatter = CustomFormatter.new 29 | 30 | result = formatter.call(time) 31 | matcher = /^\d{4}\-\d{2}\-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}$/ 32 | 33 | assert_match(matcher, result) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/logger/severity.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Logger 4 | # Logging severity. 5 | module Severity 6 | # Low-level information, mostly for developers. 7 | DEBUG = 0 8 | # Generic (useful) information about system operation. 9 | INFO = 1 10 | # A warning. 11 | WARN = 2 12 | # A handleable error condition. 13 | ERROR = 3 14 | # An unhandleable error that results in a program crash. 15 | FATAL = 4 16 | # An unknown message that should always be logged. 17 | UNKNOWN = 5 18 | 19 | LEVELS = { 20 | "debug" => DEBUG, 21 | "info" => INFO, 22 | "warn" => WARN, 23 | "error" => ERROR, 24 | "fatal" => FATAL, 25 | "unknown" => UNKNOWN, 26 | } 27 | private_constant :LEVELS 28 | 29 | def self.coerce(severity) 30 | if severity.is_a?(Integer) 31 | severity 32 | else 33 | key = severity.to_s.downcase 34 | LEVELS[key] || raise(ArgumentError, "invalid log level: #{severity}") 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | engine: cruby-truffleruby 10 | min_version: 2.5 11 | 12 | test: 13 | needs: ruby-versions 14 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 15 | strategy: 16 | matrix: 17 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 18 | os: [ ubuntu-latest, macos-latest, windows-latest ] 19 | exclude: 20 | - ruby: 2.5 21 | os: macos-latest 22 | - ruby: truffleruby 23 | os: windows-latest 24 | - ruby: truffleruby-head 25 | os: windows-latest 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - uses: actions/checkout@v6 29 | - name: Set up Ruby 30 | uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: ${{ matrix.ruby }} 33 | - name: Install dependencies 34 | run: bundle install 35 | - name: Run test 36 | run: rake test 37 | -------------------------------------------------------------------------------- /logger.gemspec: -------------------------------------------------------------------------------- 1 | begin 2 | require_relative "lib/logger/version" 3 | rescue LoadError # Fallback to load version file in ruby core repository 4 | require_relative "version" 5 | end 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "logger" 9 | spec.version = Logger::VERSION 10 | spec.authors = ["Naotoshi Seo", "SHIBATA Hiroshi"] 11 | spec.email = ["sonots@gmail.com", "hsbt@ruby-lang.org"] 12 | 13 | spec.summary = %q{Provides a simple logging utility for outputting messages.} 14 | spec.description = %q{Provides a simple logging utility for outputting messages.} 15 | spec.homepage = "https://github.com/ruby/logger" 16 | spec.licenses = ["Ruby", "BSD-2-Clause"] 17 | 18 | # Specify which files should be added to the gem when it is released. 19 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 20 | pattern = %W[ 21 | :(glob,top)* :/exe :/ext :/lib 22 | :^/.git* :^/Gemfile* :^/Rakefile* 23 | :(literal,top,exclude)#{File.basename(__FILE__)} 24 | ] 25 | spec.files = IO.popen(%w[git ls-files -z] + pattern, chdir: __dir__, &:read).split("\0") 26 | spec.require_paths = ["lib"] 27 | 28 | spec.required_ruby_version = ">= 2.5.0" 29 | 30 | spec.metadata["changelog_uri"] = spec.homepage + "/releases" 31 | end 32 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/logger' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/logger 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 27 | with: 28 | egress-policy: audit 29 | 30 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 31 | 32 | - name: Set up Ruby 33 | uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # v1.267.0 34 | with: 35 | bundler-cache: true 36 | ruby-version: ruby 37 | 38 | - name: Publish to RubyGems 39 | uses: rubygems/release-gem@1c162a739e8b4cb21a676e97b087e8268d8fc40b # v1.1.2 40 | 41 | - name: Create GitHub release 42 | run: | 43 | tag_name="$(git describe --tags --abbrev=0)" 44 | gh release create "${tag_name}" --verify-tag --generate-notes 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /lib/logger/period.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Logger 4 | module Period 5 | module_function 6 | 7 | SiD = 24 * 60 * 60 8 | 9 | def next_rotate_time(now, shift_age) 10 | case shift_age 11 | when 'daily', :daily 12 | t = Time.mktime(now.year, now.month, now.mday) + SiD 13 | when 'weekly', :weekly 14 | t = Time.mktime(now.year, now.month, now.mday) + SiD * (7 - now.wday) 15 | when 'monthly', :monthly 16 | t = Time.mktime(now.year, now.month, 1) + SiD * 32 17 | return Time.mktime(t.year, t.month, 1) 18 | when 'now', 'everytime', :now, :everytime 19 | return now 20 | else 21 | raise ArgumentError, "invalid :shift_age #{shift_age.inspect}, should be daily, weekly, monthly, or everytime" 22 | end 23 | if t.hour.nonzero? or t.min.nonzero? or t.sec.nonzero? 24 | hour = t.hour 25 | t = Time.mktime(t.year, t.month, t.mday) 26 | t += SiD if hour > 12 27 | end 28 | t 29 | end 30 | 31 | def previous_period_end(now, shift_age) 32 | case shift_age 33 | when 'daily', :daily 34 | t = Time.mktime(now.year, now.month, now.mday) - SiD / 2 35 | when 'weekly', :weekly 36 | t = Time.mktime(now.year, now.month, now.mday) - (SiD * now.wday + SiD / 2) 37 | when 'monthly', :monthly 38 | t = Time.mktime(now.year, now.month, 1) - SiD / 2 39 | when 'now', 'everytime', :now, :everytime 40 | return now 41 | else 42 | raise ArgumentError, "invalid :shift_age #{shift_age.inspect}, should be daily, weekly, monthly, or everytime" 43 | end 44 | Time.mktime(t.year, t.month, t.mday, 23, 59, 59) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Logger 2 | 3 | Logger is a simple but powerful logging utility to output messages in your Ruby program. 4 | 5 | Logger has the following features: 6 | 7 | * Print messages to different levels such as `info` and `error` 8 | * Auto-rolling of log files 9 | * Setting the format of log messages 10 | * Specifying a program name in conjunction with the message 11 | 12 | ## Installation 13 | 14 | Add this line to your application's Gemfile: 15 | 16 | ```ruby 17 | gem 'logger' 18 | ``` 19 | 20 | And then execute: 21 | 22 | $ bundle 23 | 24 | Or install it yourself as: 25 | 26 | $ gem install logger 27 | 28 | ## Usage 29 | 30 | ### Simple Example 31 | 32 | ```ruby 33 | require 'logger' 34 | 35 | # Create a Logger that prints to STDOUT 36 | log = Logger.new(STDOUT) 37 | log.debug("Created Logger") 38 | 39 | log.info("Program finished") 40 | 41 | # Create a Logger that prints to STDERR 42 | error_log = Logger.new(STDERR) 43 | error_log = error_log.error("fatal error") 44 | ``` 45 | 46 | ## Development 47 | 48 | After checking out the repo, run the following to install dependencies. 49 | 50 | ``` 51 | $ bin/setup 52 | ``` 53 | 54 | Then, run the tests as: 55 | 56 | ``` 57 | $ rake test 58 | ``` 59 | 60 | To install this gem onto your local machine, run 61 | 62 | ``` 63 | $ rake install 64 | ``` 65 | 66 | To release a new version, update the version number in `lib/logger/version.rb`, and then run 67 | 68 | ``` 69 | $ rake release 70 | ``` 71 | 72 | which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 73 | 74 | ## Advanced Development 75 | 76 | ### Run tests of a specific file 77 | 78 | ``` 79 | $ ruby test/logger/test_logger.rb 80 | ``` 81 | 82 | ### Run tests filtering test methods by a name 83 | 84 | `--name` option is available as: 85 | 86 | ``` 87 | $ ruby test/logger/test_logger.rb --name test_lshift 88 | ``` 89 | 90 | ### Publish documents to GitHub Pages 91 | 92 | ``` 93 | $ rake gh-pages 94 | ``` 95 | 96 | Then, git commit and push the generated HTMLs onto `gh-pages` branch. 97 | 98 | ## Contributing 99 | 100 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/logger. 101 | 102 | ## License 103 | 104 | The gem is available as open source under the terms of the [BSD-2-Clause](BSDL). 105 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /test/logger/test_logperiod.rb: -------------------------------------------------------------------------------- 1 | # coding: US-ASCII 2 | # frozen_string_literal: false 3 | require "logger" 4 | require "time" 5 | 6 | class TestLogPeriod < Test::Unit::TestCase 7 | def test_next_rotate_time 8 | time = Time.parse("2019-07-18 13:52:02") 9 | 10 | assert_next_rotate_time_words(time, "2019-07-19 00:00:00", ["daily", :daily]) 11 | assert_next_rotate_time_words(time, "2019-07-21 00:00:00", ["weekly", :weekly]) 12 | assert_next_rotate_time_words(time, "2019-08-01 00:00:00", ["monthly", :monthly]) 13 | 14 | assert_raise(ArgumentError) { Logger::Period.next_rotate_time(time, "invalid") } 15 | end 16 | 17 | def test_next_rotate_time_extreme_cases 18 | # First day of Month and Saturday 19 | time = Time.parse("2018-07-01 00:00:00") 20 | 21 | assert_next_rotate_time_words(time, "2018-07-02 00:00:00", ["daily", :daily]) 22 | assert_next_rotate_time_words(time, "2018-07-08 00:00:00", ["weekly", :weekly]) 23 | assert_next_rotate_time_words(time, "2018-08-01 00:00:00", ["monthly", :monthly]) 24 | 25 | assert_raise(ArgumentError) { Logger::Period.next_rotate_time(time, "invalid") } 26 | end 27 | 28 | def test_previous_period_end 29 | time = Time.parse("2019-07-18 13:52:02") 30 | 31 | assert_previous_period_end_words(time, "2019-07-17 23:59:59", ["daily", :daily]) 32 | assert_previous_period_end_words(time, "2019-07-13 23:59:59", ["weekly", :weekly]) 33 | assert_previous_period_end_words(time, "2019-06-30 23:59:59", ["monthly", :monthly]) 34 | 35 | assert_raise(ArgumentError) { Logger::Period.previous_period_end(time, "invalid") } 36 | end 37 | 38 | def test_previous_period_end_extreme_cases 39 | # First day of Month and Saturday 40 | time = Time.parse("2018-07-01 00:00:00") 41 | previous_date = "2018-06-30 23:59:59" 42 | 43 | assert_previous_period_end_words(time, previous_date, ["daily", :daily]) 44 | assert_previous_period_end_words(time, previous_date, ["weekly", :weekly]) 45 | assert_previous_period_end_words(time, previous_date, ["monthly", :monthly]) 46 | 47 | assert_raise(ArgumentError) { Logger::Period.previous_period_end(time, "invalid") } 48 | end 49 | 50 | private 51 | 52 | def assert_next_rotate_time_words(time, next_date, words) 53 | assert_time_words(:next_rotate_time, time, next_date, words) 54 | end 55 | 56 | def assert_previous_period_end_words(time, previous_date, words) 57 | assert_time_words(:previous_period_end, time, previous_date, words) 58 | end 59 | 60 | def assert_time_words(method, time, date, words) 61 | words.each do |word| 62 | daily_result = Logger::Period.public_send(method, time, word) 63 | expected_result = Time.parse(date) 64 | assert_equal(expected_result, daily_result) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/logger/test_severity.rb: -------------------------------------------------------------------------------- 1 | # coding: US-ASCII 2 | # frozen_string_literal: false 3 | require 'logger' 4 | 5 | class TestLoggerSeverity < Test::Unit::TestCase 6 | include Logger::Severity 7 | 8 | def test_enum 9 | logger_levels = Logger.constants 10 | levels = ["WARN", "UNKNOWN", "INFO", "FATAL", "DEBUG", "ERROR"] 11 | Logger::Severity.constants.each do |level| 12 | assert(levels.include?(level.to_s)) 13 | assert(logger_levels.include?(level)) 14 | end 15 | assert_equal(levels.size, Logger::Severity.constants.size) 16 | end 17 | 18 | def test_level_assignment 19 | logger = Logger.new(nil) 20 | 21 | Logger::Severity.constants.each do |level| 22 | next if level == :UNKNOWN 23 | 24 | logger.send("#{level.downcase}!") 25 | assert(logger.level) == Logger::Severity.const_get(level) 26 | end 27 | end 28 | 29 | def test_fiber_local_level 30 | logger = Logger.new(nil) 31 | logger.level = INFO # default level 32 | other = Logger.new(nil) 33 | other.level = ERROR # default level 34 | 35 | assert_equal(other.level, ERROR) 36 | logger.with_level(:WARN) do 37 | assert_equal(other.level, ERROR) 38 | assert_equal(logger.level, WARN) 39 | 40 | logger.with_level(DEBUG) do # verify reentrancy 41 | assert_equal(logger.level, DEBUG) 42 | 43 | Fiber.new do 44 | assert_equal(logger.level, INFO) 45 | logger.with_level(:WARN) do 46 | assert_equal(other.level, ERROR) 47 | assert_equal(logger.level, WARN) 48 | end 49 | assert_equal(logger.level, INFO) 50 | end.resume 51 | 52 | assert_equal(logger.level, DEBUG) 53 | end 54 | assert_equal(logger.level, WARN) 55 | end 56 | assert_equal(logger.level, INFO) 57 | end 58 | 59 | def test_thread_local_level 60 | subclass = Class.new(Logger) do 61 | def level_key 62 | Thread.current 63 | end 64 | end 65 | 66 | logger = subclass.new(nil) 67 | logger.level = INFO # default level 68 | other = subclass.new(nil) 69 | other.level = ERROR # default level 70 | 71 | assert_equal(other.level, ERROR) 72 | logger.with_level(:WARN) do 73 | assert_equal(other.level, ERROR) 74 | assert_equal(logger.level, WARN) 75 | 76 | logger.with_level(DEBUG) do # verify reentrancy 77 | assert_equal(logger.level, DEBUG) 78 | 79 | Fiber.new do 80 | assert_equal(logger.level, DEBUG) 81 | logger.with_level(:WARN) do 82 | assert_equal(other.level, ERROR) 83 | assert_equal(logger.level, WARN) 84 | end 85 | assert_equal(logger.level, DEBUG) 86 | end.resume 87 | 88 | assert_equal(logger.level, DEBUG) 89 | end 90 | assert_equal(logger.level, WARN) 91 | end 92 | assert_equal(logger.level, INFO) 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/logger/log_device.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'period' 4 | 5 | class Logger 6 | # Device used for logging messages. 7 | class LogDevice 8 | include Period 9 | 10 | attr_reader :dev 11 | attr_reader :filename 12 | include MonitorMixin 13 | 14 | def initialize( 15 | log = nil, shift_age: nil, shift_size: nil, shift_period_suffix: nil, 16 | binmode: false, reraise_write_errors: [], skip_header: false 17 | ) 18 | @dev = @filename = @shift_age = @shift_size = @shift_period_suffix = nil 19 | @binmode = binmode 20 | @reraise_write_errors = reraise_write_errors 21 | @skip_header = skip_header 22 | mon_initialize 23 | set_dev(log) 24 | set_file(shift_age, shift_size, shift_period_suffix) if @filename 25 | end 26 | 27 | def write(message) 28 | handle_write_errors("writing") do 29 | synchronize do 30 | if @shift_age and @dev.respond_to?(:stat) 31 | handle_write_errors("shifting") {check_shift_log} 32 | end 33 | handle_write_errors("writing") {@dev.write(message)} 34 | end 35 | end 36 | end 37 | 38 | def close 39 | begin 40 | synchronize do 41 | @dev.close rescue nil 42 | end 43 | rescue Exception 44 | @dev.close rescue nil 45 | end 46 | end 47 | 48 | def reopen(log = nil, shift_age: nil, shift_size: nil, shift_period_suffix: nil, binmode: nil) 49 | # reopen the same filename if no argument, do nothing for IO 50 | log ||= @filename if @filename 51 | @binmode = binmode unless binmode.nil? 52 | if log 53 | synchronize do 54 | if @filename and @dev 55 | @dev.close rescue nil # close only file opened by Logger 56 | @filename = nil 57 | end 58 | set_dev(log) 59 | set_file(shift_age, shift_size, shift_period_suffix) if @filename 60 | end 61 | end 62 | self 63 | end 64 | 65 | private 66 | 67 | # :stopdoc: 68 | 69 | MODE = File::WRONLY | File::APPEND 70 | # TruffleRuby < 24.2 does not have File::SHARE_DELETE 71 | if File.const_defined? :SHARE_DELETE 72 | MODE_TO_OPEN = MODE | File::SHARE_DELETE | File::BINARY 73 | else 74 | MODE_TO_OPEN = MODE | File::BINARY 75 | end 76 | MODE_TO_CREATE = MODE_TO_OPEN | File::CREAT | File::EXCL 77 | 78 | def set_dev(log) 79 | if log.respond_to?(:write) and log.respond_to?(:close) 80 | @dev = log 81 | if log.respond_to?(:path) and path = log.path 82 | if File.exist?(path) 83 | @filename = path 84 | end 85 | end 86 | else 87 | @dev = open_logfile(log) 88 | @filename = log 89 | end 90 | end 91 | 92 | def set_file(shift_age, shift_size, shift_period_suffix) 93 | @shift_age = shift_age || @shift_age || 7 94 | @shift_size = shift_size || @shift_size || 1048576 95 | @shift_period_suffix = shift_period_suffix || @shift_period_suffix || '%Y%m%d' 96 | 97 | unless @shift_age.is_a?(Integer) 98 | base_time = @dev.respond_to?(:stat) ? @dev.stat.mtime : Time.now 99 | @next_rotate_time = next_rotate_time(base_time, @shift_age) 100 | end 101 | end 102 | 103 | if MODE_TO_OPEN == MODE 104 | def fixup_mode(dev) 105 | dev 106 | end 107 | else 108 | def fixup_mode(dev) 109 | return dev if @binmode 110 | dev.autoclose = false 111 | old_dev = dev 112 | dev = File.new(dev.fileno, mode: MODE, path: dev.path) 113 | old_dev.close 114 | PathAttr.set_path(dev, filename) if defined?(PathAttr) 115 | dev 116 | end 117 | end 118 | 119 | def open_logfile(filename) 120 | begin 121 | dev = File.open(filename, MODE_TO_OPEN) 122 | rescue Errno::ENOENT 123 | create_logfile(filename) 124 | else 125 | dev = fixup_mode(dev) 126 | dev.sync = true 127 | dev.binmode if @binmode 128 | dev 129 | end 130 | end 131 | 132 | def create_logfile(filename) 133 | begin 134 | logdev = File.open(filename, MODE_TO_CREATE) 135 | logdev.flock(File::LOCK_EX) 136 | logdev = fixup_mode(logdev) 137 | logdev.sync = true 138 | logdev.binmode if @binmode 139 | add_log_header(logdev) unless @skip_header 140 | logdev.flock(File::LOCK_UN) 141 | logdev 142 | rescue Errno::EEXIST 143 | # file is created by another process 144 | open_logfile(filename) 145 | end 146 | end 147 | 148 | def handle_write_errors(mesg) 149 | yield 150 | rescue *@reraise_write_errors 151 | raise 152 | rescue 153 | warn("log #{mesg} failed. #{$!}") 154 | end 155 | 156 | def add_log_header(file) 157 | file.write( 158 | "# Logfile created on %s by %s\n" % [Time.now.to_s, Logger::ProgName] 159 | ) if file.size == 0 160 | end 161 | 162 | def check_shift_log 163 | if @shift_age.is_a?(Integer) 164 | # Note: always returns false if '0'. 165 | if @filename && (@shift_age > 0) && (@dev.stat.size > @shift_size) 166 | lock_shift_log { shift_log_age } 167 | end 168 | else 169 | now = Time.now 170 | if now >= @next_rotate_time 171 | @next_rotate_time = next_rotate_time(now, @shift_age) 172 | lock_shift_log { shift_log_period(previous_period_end(now, @shift_age)) } 173 | end 174 | end 175 | end 176 | 177 | def lock_shift_log 178 | retry_limit = 8 179 | retry_sleep = 0.1 180 | begin 181 | File.open(@filename, MODE_TO_OPEN) do |lock| 182 | lock.flock(File::LOCK_EX) # inter-process locking. will be unlocked at closing file 183 | if File.identical?(@filename, lock) and File.identical?(lock, @dev) 184 | yield # log shifting 185 | else 186 | # log shifted by another process (i-node before locking and i-node after locking are different) 187 | @dev.close rescue nil 188 | @dev = open_logfile(@filename) 189 | end 190 | end 191 | true 192 | rescue Errno::ENOENT 193 | # @filename file would not exist right after #rename and before #create_logfile 194 | if retry_limit <= 0 195 | warn("log rotation inter-process lock failed. #{$!}") 196 | else 197 | sleep retry_sleep 198 | retry_limit -= 1 199 | retry_sleep *= 2 200 | retry 201 | end 202 | end 203 | rescue 204 | warn("log rotation inter-process lock failed. #{$!}") 205 | end 206 | 207 | def shift_log_age 208 | (@shift_age-3).downto(0) do |i| 209 | if FileTest.exist?("#{@filename}.#{i}") 210 | File.rename("#{@filename}.#{i}", "#{@filename}.#{i+1}") 211 | end 212 | end 213 | shift_log_file("#{@filename}.0") 214 | end 215 | 216 | def shift_log_period(period_end) 217 | suffix = period_end.strftime(@shift_period_suffix) 218 | age_file = "#{@filename}.#{suffix}" 219 | if FileTest.exist?(age_file) 220 | # try to avoid filename crash caused by Timestamp change. 221 | idx = 0 222 | # .99 can be overridden; avoid too much file search with 'loop do' 223 | while idx < 100 224 | idx += 1 225 | age_file = "#{@filename}.#{suffix}.#{idx}" 226 | break unless FileTest.exist?(age_file) 227 | end 228 | end 229 | shift_log_file(age_file) 230 | end 231 | 232 | def shift_log_file(shifted) 233 | stat = @dev.stat 234 | @dev.close rescue nil 235 | File.rename(@filename, shifted) 236 | @dev = create_logfile(@filename) 237 | mode, uid, gid = stat.mode, stat.uid, stat.gid 238 | begin 239 | @dev.chmod(mode) if mode 240 | mode = nil 241 | @dev.chown(uid, gid) 242 | rescue Errno::EPERM 243 | if mode 244 | # failed to chmod, probably nothing can do more. 245 | elsif uid 246 | uid = nil 247 | retry # to change gid only 248 | end 249 | end 250 | return true 251 | end 252 | end 253 | end 254 | 255 | File.open(__FILE__) do |f| 256 | File.new(f.fileno, autoclose: false, path: "").path 257 | rescue IOError 258 | module PathAttr # :nodoc: 259 | attr_reader :path 260 | 261 | def self.set_path(file, path) 262 | file.extend(self).instance_variable_set(:@path, path) 263 | end 264 | end 265 | end 266 | -------------------------------------------------------------------------------- /test/logger/test_logger.rb: -------------------------------------------------------------------------------- 1 | # coding: US-ASCII 2 | # frozen_string_literal: false 3 | require 'logger' 4 | require 'tempfile' 5 | 6 | class TestLogger < Test::Unit::TestCase 7 | include Logger::Severity 8 | 9 | def setup 10 | @logger = Logger.new(nil) 11 | end 12 | 13 | class Log 14 | attr_reader :label, :datetime, :pid, :severity, :progname, :msg 15 | def initialize(line) 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 | Tempfile.create(File.basename(__FILE__) + '.log') {|logdev| 31 | logger.instance_eval { @logdev = Logger::LogDevice.new(logdev) } 32 | logger.__send__(msg_id, *arg, &block) 33 | logdev.rewind 34 | logdev.read 35 | } 36 | end 37 | 38 | def test_level 39 | @logger.level = UNKNOWN 40 | assert_equal(UNKNOWN, @logger.level) 41 | @logger.level = INFO 42 | assert_equal(INFO, @logger.level) 43 | @logger.sev_threshold = ERROR 44 | assert_equal(ERROR, @logger.sev_threshold) 45 | @logger.sev_threshold = WARN 46 | assert_equal(WARN, @logger.sev_threshold) 47 | assert_equal(WARN, @logger.level) 48 | 49 | @logger.level = DEBUG 50 | assert(@logger.debug?) 51 | assert(@logger.info?) 52 | @logger.level = INFO 53 | assert(!@logger.debug?) 54 | assert(@logger.info?) 55 | assert(@logger.warn?) 56 | @logger.level = WARN 57 | assert(!@logger.info?) 58 | assert(@logger.warn?) 59 | assert(@logger.error?) 60 | @logger.level = ERROR 61 | assert(!@logger.warn?) 62 | assert(@logger.error?) 63 | assert(@logger.fatal?) 64 | @logger.level = FATAL 65 | assert(!@logger.error?) 66 | assert(@logger.fatal?) 67 | @logger.level = UNKNOWN 68 | assert(!@logger.error?) 69 | assert(!@logger.fatal?) 70 | end 71 | 72 | def test_symbol_level 73 | logger_symbol_levels = { 74 | debug: DEBUG, 75 | info: INFO, 76 | warn: WARN, 77 | error: ERROR, 78 | fatal: FATAL, 79 | unknown: UNKNOWN, 80 | DEBUG: DEBUG, 81 | INFO: INFO, 82 | WARN: WARN, 83 | ERROR: ERROR, 84 | FATAL: FATAL, 85 | UNKNOWN: UNKNOWN, 86 | } 87 | logger_symbol_levels.each do |symbol, level| 88 | @logger.level = symbol 89 | assert(@logger.level == level) 90 | end 91 | assert_raise(ArgumentError) { @logger.level = :something_wrong } 92 | end 93 | 94 | def test_string_level 95 | logger_string_levels = { 96 | 'debug' => DEBUG, 97 | 'info' => INFO, 98 | 'warn' => WARN, 99 | 'error' => ERROR, 100 | 'fatal' => FATAL, 101 | 'unknown' => UNKNOWN, 102 | 'DEBUG' => DEBUG, 103 | 'INFO' => INFO, 104 | 'WARN' => WARN, 105 | 'ERROR' => ERROR, 106 | 'FATAL' => FATAL, 107 | 'UNKNOWN' => UNKNOWN, 108 | } 109 | logger_string_levels.each do |string, level| 110 | @logger.level = string 111 | assert(@logger.level == level) 112 | end 113 | assert_raise(ArgumentError) { @logger.level = 'something_wrong' } 114 | end 115 | 116 | def test_reraise_write_errors 117 | c = Object.new 118 | e = Class.new(StandardError) 119 | c.define_singleton_method(:write){|*| raise e} 120 | c.define_singleton_method(:close){} 121 | logger = Logger.new(c, :reraise_write_errors=>[e]) 122 | assert_raise(e) { logger.warn('foo') } 123 | end 124 | 125 | def test_progname 126 | assert_nil(@logger.progname) 127 | @logger.progname = "name" 128 | assert_equal("name", @logger.progname) 129 | end 130 | 131 | def test_datetime_format 132 | verbose, $VERBOSE = $VERBOSE, false 133 | dummy = STDERR 134 | logger = Logger.new(dummy) 135 | log = log_add(logger, INFO, "foo") 136 | assert_match(/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d.\s*\d+$/, log.datetime) 137 | logger.datetime_format = "%d%b%Y@%H:%M:%S" 138 | log = log_add(logger, INFO, "foo") 139 | assert_match(/^\d\d\w\w\w\d\d\d\d@\d\d:\d\d:\d\d$/, log.datetime) 140 | logger.datetime_format = "" 141 | log = log_add(logger, INFO, "foo") 142 | assert_match(/^$/, log.datetime) 143 | ensure 144 | $VERBOSE = verbose 145 | end 146 | 147 | def test_formatter 148 | dummy = STDERR 149 | logger = Logger.new(dummy) 150 | # default 151 | log = log(logger, :info, "foo") 152 | assert_equal("foo\n", log.msg) 153 | # config 154 | logger.formatter = proc { |severity, timestamp, progname, msg| 155 | "#{severity}:#{msg}\n\n" 156 | } 157 | line = log_raw(logger, :info, "foo") 158 | assert_equal("INFO:foo\n\n", line) 159 | # recover 160 | logger.formatter = nil 161 | log = log(logger, :info, "foo") 162 | assert_equal("foo\n", log.msg) 163 | # again 164 | o = Object.new 165 | def o.call(severity, timestamp, progname, msg) 166 | "<<#{severity}-#{msg}>>\n" 167 | end 168 | logger.formatter = o 169 | line = log_raw(logger, :info, "foo") 170 | assert_equal("<"">\n", line) 171 | end 172 | 173 | def test_initialize 174 | logger = Logger.new(STDERR) 175 | assert_nil(logger.progname) 176 | assert_equal(DEBUG, logger.level) 177 | assert_nil(logger.datetime_format) 178 | end 179 | 180 | def test_logdev 181 | logger = Logger.new(STDERR) 182 | assert_instance_of(Logger::LogDevice, logger.logdev) 183 | 184 | logdev = Logger::LogDevice.new(STDERR) 185 | logger = Logger.new(logdev) 186 | assert_instance_of(Logger::LogDevice, logger.logdev) 187 | assert_equal(STDERR, logger.logdev.dev) 188 | end 189 | 190 | def test_initialize_with_level 191 | # default 192 | logger = Logger.new(STDERR) 193 | assert_equal(Logger::DEBUG, logger.level) 194 | # config 195 | logger = Logger.new(STDERR, level: :info) 196 | assert_equal(Logger::INFO, logger.level) 197 | end 198 | 199 | def test_initialize_with_progname 200 | # default 201 | logger = Logger.new(STDERR) 202 | assert_equal(nil, logger.progname) 203 | # config 204 | logger = Logger.new(STDERR, progname: :progname) 205 | assert_equal(:progname, logger.progname) 206 | end 207 | 208 | def test_initialize_with_formatter 209 | # default 210 | logger = Logger.new(STDERR) 211 | log = log(logger, :info, "foo") 212 | assert_equal("foo\n", log.msg) 213 | # config 214 | logger = Logger.new(STDERR, formatter: proc { |severity, timestamp, progname, msg| 215 | "#{severity}:#{msg}\n\n" 216 | }) 217 | line = log_raw(logger, :info, "foo") 218 | assert_equal("INFO:foo\n\n", line) 219 | end 220 | 221 | def test_initialize_with_datetime_format 222 | # default 223 | logger = Logger.new(STDERR) 224 | log = log_add(logger, INFO, "foo") 225 | assert_match(/^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d.\s*\d+$/, log.datetime) 226 | # config 227 | logger = Logger.new(STDERR, datetime_format: "%d%b%Y@%H:%M:%S") 228 | log = log_add(logger, INFO, "foo") 229 | assert_match(/^\d\d\w\w\w\d\d\d\d@\d\d:\d\d:\d\d$/, log.datetime) 230 | end 231 | 232 | def test_reopen 233 | logger = Logger.new(STDERR) 234 | logger.reopen(STDOUT) 235 | assert_equal(STDOUT, logger.instance_variable_get(:@logdev).dev) 236 | end 237 | 238 | def test_reopen_nil_logdevice 239 | logger = Logger.new(File::NULL) 240 | assert_nothing_raised do 241 | logger.reopen(STDOUT) 242 | end 243 | end 244 | 245 | def test_add 246 | logger = Logger.new(nil) 247 | logger.progname = "my_progname" 248 | assert(logger.add(INFO)) 249 | log = log_add(logger, nil, "msg") 250 | assert_equal("ANY", log.severity) 251 | assert_equal("my_progname", log.progname) 252 | logger.level = WARN 253 | assert(logger.log(INFO)) 254 | assert_nil(log_add(logger, INFO, "msg").msg) 255 | log = log_add(logger, WARN, nil) { "msg" } 256 | assert_equal("msg\n", log.msg) 257 | log = log_add(logger, WARN, "") { "msg" } 258 | assert_equal("\n", log.msg) 259 | assert_equal("my_progname", log.progname) 260 | log = log_add(logger, WARN, nil, "progname?") 261 | assert_equal("progname?\n", log.msg) 262 | assert_equal("my_progname", log.progname) 263 | # 264 | logger = Logger.new(nil) 265 | log = log_add(logger, INFO, nil, false) 266 | assert_equal("false\n", log.msg) 267 | end 268 | 269 | def test_add_binary_data_with_binmode_logdev 270 | EnvUtil.with_default_internal(Encoding::UTF_8) do 271 | begin 272 | tempfile = Tempfile.new("logger") 273 | tempfile.close 274 | filename = tempfile.path 275 | File.unlink(filename) 276 | 277 | logger = Logger.new filename, binmode: true 278 | logger.level = Logger::DEBUG 279 | 280 | str = +"\x80" 281 | str.force_encoding("ASCII-8BIT") 282 | 283 | logger.add Logger::DEBUG, str 284 | assert_equal(2, File.binread(filename).split(/\n/).size) 285 | ensure 286 | logger.close 287 | tempfile.unlink 288 | end 289 | end 290 | end 291 | 292 | def test_level_log 293 | logger = Logger.new(nil) 294 | logger.progname = "my_progname" 295 | log = log(logger, :debug, "custom_progname") { "msg" } 296 | assert_equal("msg\n", log.msg) 297 | assert_equal("custom_progname", log.progname) 298 | assert_equal("DEBUG", log.severity) 299 | assert_equal("D", log.label) 300 | # 301 | log = log(logger, :debug) { "msg_block" } 302 | assert_equal("msg_block\n", log.msg) 303 | assert_equal("my_progname", log.progname) 304 | log = log(logger, :debug, "msg_inline") 305 | assert_equal("msg_inline\n", log.msg) 306 | assert_equal("my_progname", log.progname) 307 | # 308 | log = log(logger, :info, "custom_progname") { "msg" } 309 | assert_equal("msg\n", log.msg) 310 | assert_equal("custom_progname", log.progname) 311 | assert_equal("INFO", log.severity) 312 | assert_equal("I", log.label) 313 | # 314 | log = log(logger, :warn, "custom_progname") { "msg" } 315 | assert_equal("msg\n", log.msg) 316 | assert_equal("custom_progname", log.progname) 317 | assert_equal("WARN", log.severity) 318 | assert_equal("W", log.label) 319 | # 320 | log = log(logger, :error, "custom_progname") { "msg" } 321 | assert_equal("msg\n", log.msg) 322 | assert_equal("custom_progname", log.progname) 323 | assert_equal("ERROR", log.severity) 324 | assert_equal("E", log.label) 325 | # 326 | log = log(logger, :fatal, "custom_progname") { "msg" } 327 | assert_equal("msg\n", log.msg) 328 | assert_equal("custom_progname", log.progname) 329 | assert_equal("FATAL", log.severity) 330 | assert_equal("F", log.label) 331 | # 332 | log = log(logger, :unknown, "custom_progname") { "msg" } 333 | assert_equal("msg\n", log.msg) 334 | assert_equal("custom_progname", log.progname) 335 | assert_equal("ANY", log.severity) 336 | assert_equal("A", log.label) 337 | end 338 | 339 | def test_close 340 | r, w = IO.pipe 341 | assert(!w.closed?) 342 | logger = Logger.new(w) 343 | logger.close 344 | assert(w.closed?) 345 | r.close 346 | end 347 | 348 | class MyError < StandardError 349 | end 350 | 351 | class MyMsg 352 | def inspect 353 | "my_msg" 354 | end 355 | end 356 | 357 | def test_format 358 | logger = Logger.new(nil) 359 | log = log_add(logger, INFO, "msg\n") 360 | assert_equal("msg\n\n", log.msg) 361 | begin 362 | raise MyError.new("excn") 363 | rescue MyError => e 364 | log = log_add(logger, INFO, e) 365 | assert_match(/^excn \(TestLogger::MyError\)/, log.msg) 366 | # expects backtrace is dumped across multi lines. 10 might be changed. 367 | assert(log.msg.split(/\n/).size >= 10) 368 | end 369 | log = log_add(logger, INFO, MyMsg.new) 370 | assert_equal("my_msg\n", log.msg) 371 | end 372 | 373 | def test_lshift 374 | r, w = IO.pipe 375 | logger = Logger.new(w) 376 | logger << "msg" 377 | IO.select([r], nil, nil, 0.1) 378 | w.close 379 | msg = r.read 380 | r.close 381 | assert_equal("msg", msg) 382 | # 383 | r, w = IO.pipe 384 | logger = Logger.new(w) 385 | logger << "msg2\n\n" 386 | IO.select([r], nil, nil, 0.1) 387 | w.close 388 | msg = r.read 389 | r.close 390 | assert_equal("msg2\n\n", msg) 391 | end 392 | 393 | class CustomLogger < Logger 394 | def level 395 | INFO 396 | end 397 | end 398 | 399 | def test_overriding_level 400 | logger = CustomLogger.new(nil) 401 | log = log(logger, :info) { "msg" } 402 | assert_equal "msg\n", log.msg 403 | # 404 | log = log(logger, :debug) { "msg" } 405 | assert_nil log.msg 406 | end 407 | 408 | def test_does_not_instantiate_log_device_for_File_NULL 409 | l = Logger.new(File::NULL) 410 | assert_nil(l.instance_variable_get(:@logdev)) 411 | end 412 | 413 | def test_subclass_initialize 414 | bad_logger = Class.new(Logger) {def initialize(*); end}.new(IO::NULL) 415 | line = __LINE__ - 1 416 | file = Regexp.quote(__FILE__) 417 | assert_warning(/not initialized properly.*\n#{file}:#{line}:/) do 418 | bad_logger.level 419 | end 420 | 421 | good_logger = Class.new(Logger) {def initialize(*); super; end}.new(IO::NULL) 422 | file = Regexp.quote(__FILE__) 423 | assert_warning('') do 424 | good_logger.level 425 | end 426 | end 427 | end 428 | -------------------------------------------------------------------------------- /lib/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # logger.rb - simple logging utility 3 | # Copyright (C) 2000-2003, 2005, 2008, 2011 NAKAMURA, Hiroshi . 4 | # 5 | # Documentation:: NAKAMURA, Hiroshi and Gavin Sinclair 6 | # License:: 7 | # You can redistribute it and/or modify it under the same terms of Ruby's 8 | # license; either the dual license version in 2003, or any later version. 9 | # Revision:: $Id$ 10 | # 11 | # A simple system for logging messages. See Logger for more documentation. 12 | 13 | require 'fiber' 14 | require 'monitor' 15 | require 'rbconfig' 16 | 17 | require_relative 'logger/version' 18 | require_relative 'logger/formatter' 19 | require_relative 'logger/log_device' 20 | require_relative 'logger/severity' 21 | require_relative 'logger/errors' 22 | 23 | # \Class \Logger provides a simple but sophisticated logging utility that 24 | # you can use to create one or more 25 | # {event logs}[https://en.wikipedia.org/wiki/Logging_(software)#Event_logs] 26 | # for your program. 27 | # Each such log contains a chronological sequence of entries 28 | # that provides a record of the program's activities. 29 | # 30 | # == About the Examples 31 | # 32 | # All examples on this page assume that \Logger has been required: 33 | # 34 | # require 'logger' 35 | # 36 | # == Synopsis 37 | # 38 | # Create a log with Logger.new: 39 | # 40 | # # Single log file. 41 | # logger = Logger.new('t.log') 42 | # # Size-based rotated logging: 3 10-megabyte files. 43 | # logger = Logger.new('t.log', 3, 10485760) 44 | # # Period-based rotated logging: daily (also allowed: 'weekly', 'monthly'). 45 | # logger = Logger.new('t.log', 'daily') 46 | # # Log to an IO stream. 47 | # logger = Logger.new($stdout) 48 | # 49 | # Add entries (level, message) with Logger#add: 50 | # 51 | # logger.add(Logger::DEBUG, 'Maximal debugging info') 52 | # logger.add(Logger::INFO, 'Non-error information') 53 | # logger.add(Logger::WARN, 'Non-error warning') 54 | # logger.add(Logger::ERROR, 'Non-fatal error') 55 | # logger.add(Logger::FATAL, 'Fatal error') 56 | # logger.add(Logger::UNKNOWN, 'Most severe') 57 | # 58 | # Close the log with Logger#close: 59 | # 60 | # logger.close 61 | # 62 | # == Entries 63 | # 64 | # You can add entries with method Logger#add: 65 | # 66 | # logger.add(Logger::DEBUG, 'Maximal debugging info') 67 | # logger.add(Logger::INFO, 'Non-error information') 68 | # logger.add(Logger::WARN, 'Non-error warning') 69 | # logger.add(Logger::ERROR, 'Non-fatal error') 70 | # logger.add(Logger::FATAL, 'Fatal error') 71 | # logger.add(Logger::UNKNOWN, 'Most severe') 72 | # 73 | # These shorthand methods also add entries: 74 | # 75 | # logger.debug('Maximal debugging info') 76 | # logger.info('Non-error information') 77 | # logger.warn('Non-error warning') 78 | # logger.error('Non-fatal error') 79 | # logger.fatal('Fatal error') 80 | # logger.unknown('Most severe') 81 | # 82 | # When you call any of these methods, 83 | # the entry may or may not be written to the log, 84 | # depending on the entry's severity and on the log level; 85 | # see {Log Level}[rdoc-ref:Logger@Log+Level] 86 | # 87 | # An entry always has: 88 | # 89 | # - A severity (the required argument to #add). 90 | # - An automatically created timestamp. 91 | # 92 | # And may also have: 93 | # 94 | # - A message. 95 | # - A program name. 96 | # 97 | # Example: 98 | # 99 | # logger = Logger.new($stdout) 100 | # logger.add(Logger::INFO, 'My message.', 'mung') 101 | # # => I, [2022-05-07T17:21:46.536234 #20536] INFO -- mung: My message. 102 | # 103 | # The default format for an entry is: 104 | # 105 | # "%s, [%s #%d] %5s -- %s: %s\n" 106 | # 107 | # where the values to be formatted are: 108 | # 109 | # - \Severity (one letter). 110 | # - Timestamp. 111 | # - Process id. 112 | # - \Severity (word). 113 | # - Program name. 114 | # - Message. 115 | # 116 | # You can use a different entry format by: 117 | # 118 | # - Setting a custom format proc (affects following entries); 119 | # see {formatter=}[Logger.html#attribute-i-formatter]. 120 | # - Calling any of the methods above with a block 121 | # (affects only the one entry). 122 | # Doing so can have two benefits: 123 | # 124 | # - Context: the block can evaluate the entire program context 125 | # and create a context-dependent message. 126 | # - Performance: the block is not evaluated unless the log level 127 | # permits the entry actually to be written: 128 | # 129 | # logger.error { my_slow_message_generator } 130 | # 131 | # Contrast this with the string form, where the string is 132 | # always evaluated, regardless of the log level: 133 | # 134 | # logger.error("#{my_slow_message_generator}") 135 | # 136 | # === \Severity 137 | # 138 | # The severity of a log entry has two effects: 139 | # 140 | # - Determines whether the entry is selected for inclusion in the log; 141 | # see {Log Level}[rdoc-ref:Logger@Log+Level]. 142 | # - Indicates to any log reader (whether a person or a program) 143 | # the relative importance of the entry. 144 | # 145 | # === Timestamp 146 | # 147 | # The timestamp for a log entry is generated automatically 148 | # when the entry is created. 149 | # 150 | # The logged timestamp is formatted by method 151 | # {Time#strftime}[https://docs.ruby-lang.org/en/master/Time.html#method-i-strftime] 152 | # using this format string: 153 | # 154 | # '%Y-%m-%dT%H:%M:%S.%6N' 155 | # 156 | # Example: 157 | # 158 | # logger = Logger.new($stdout) 159 | # logger.add(Logger::INFO) 160 | # # => I, [2022-05-07T17:04:32.318331 #20536] INFO -- : nil 161 | # 162 | # You can set a different format using method #datetime_format=. 163 | # 164 | # === Message 165 | # 166 | # The message is an optional argument to an entry method: 167 | # 168 | # logger = Logger.new($stdout) 169 | # logger.add(Logger::INFO, 'My message') 170 | # # => I, [2022-05-07T18:15:37.647581 #20536] INFO -- : My message 171 | # 172 | # For the default entry formatter, Logger::Formatter, 173 | # the message object may be: 174 | # 175 | # - A string: used as-is. 176 | # - An Exception: message.message is used. 177 | # - Anything else: message.inspect is used. 178 | # 179 | # *Note*: Logger::Formatter does not escape or sanitize 180 | # the message passed to it. 181 | # Developers should be aware that malicious data (user input) 182 | # may be in the message, and should explicitly escape untrusted data. 183 | # 184 | # You can use a custom formatter to escape message data; 185 | # see the example at {formatter=}[Logger.html#attribute-i-formatter]. 186 | # 187 | # === Program Name 188 | # 189 | # The program name is an optional argument to an entry method: 190 | # 191 | # logger = Logger.new($stdout) 192 | # logger.add(Logger::INFO, 'My message', 'mung') 193 | # # => I, [2022-05-07T18:17:38.084716 #20536] INFO -- mung: My message 194 | # 195 | # The default program name for a new logger may be set in the call to 196 | # Logger.new via optional keyword argument +progname+: 197 | # 198 | # logger = Logger.new('t.log', progname: 'mung') 199 | # 200 | # The default program name for an existing logger may be set 201 | # by a call to method #progname=: 202 | # 203 | # logger.progname = 'mung' 204 | # 205 | # The current program name may be retrieved with method 206 | # {progname}[Logger.html#attribute-i-progname]: 207 | # 208 | # logger.progname # => "mung" 209 | # 210 | # == Log Level 211 | # 212 | # The log level setting determines whether an entry is actually 213 | # written to the log, based on the entry's severity. 214 | # 215 | # These are the defined severities (least severe to most severe): 216 | # 217 | # logger = Logger.new($stdout) 218 | # logger.add(Logger::DEBUG, 'Maximal debugging info') 219 | # # => D, [2022-05-07T17:57:41.776220 #20536] DEBUG -- : Maximal debugging info 220 | # logger.add(Logger::INFO, 'Non-error information') 221 | # # => I, [2022-05-07T17:59:14.349167 #20536] INFO -- : Non-error information 222 | # logger.add(Logger::WARN, 'Non-error warning') 223 | # # => W, [2022-05-07T18:00:45.337538 #20536] WARN -- : Non-error warning 224 | # logger.add(Logger::ERROR, 'Non-fatal error') 225 | # # => E, [2022-05-07T18:02:41.592912 #20536] ERROR -- : Non-fatal error 226 | # logger.add(Logger::FATAL, 'Fatal error') 227 | # # => F, [2022-05-07T18:05:24.703931 #20536] FATAL -- : Fatal error 228 | # logger.add(Logger::UNKNOWN, 'Most severe') 229 | # # => A, [2022-05-07T18:07:54.657491 #20536] ANY -- : Most severe 230 | # 231 | # The default initial level setting is Logger::DEBUG, the lowest level, 232 | # which means that all entries are to be written, regardless of severity: 233 | # 234 | # logger = Logger.new($stdout) 235 | # logger.level # => 0 236 | # logger.add(0, "My message") 237 | # # => D, [2022-05-11T15:10:59.773668 #20536] DEBUG -- : My message 238 | # 239 | # You can specify a different setting in a new logger 240 | # using keyword argument +level+ with an appropriate value: 241 | # 242 | # logger = Logger.new($stdout, level: Logger::ERROR) 243 | # logger = Logger.new($stdout, level: 'error') 244 | # logger = Logger.new($stdout, level: :error) 245 | # logger.level # => 3 246 | # 247 | # With this level, entries with severity Logger::ERROR and higher 248 | # are written, while those with lower severities are not written: 249 | # 250 | # logger = Logger.new($stdout, level: Logger::ERROR) 251 | # logger.add(3) 252 | # # => E, [2022-05-11T15:17:20.933362 #20536] ERROR -- : nil 253 | # logger.add(2) # Silent. 254 | # 255 | # You can set the log level for an existing logger 256 | # with method #level=: 257 | # 258 | # logger.level = Logger::ERROR 259 | # 260 | # These shorthand methods also set the level: 261 | # 262 | # logger.debug! # => 0 263 | # logger.info! # => 1 264 | # logger.warn! # => 2 265 | # logger.error! # => 3 266 | # logger.fatal! # => 4 267 | # 268 | # You can retrieve the log level with method #level. 269 | # 270 | # logger.level = Logger::ERROR 271 | # logger.level # => 3 272 | # 273 | # These methods return whether a given 274 | # level is to be written: 275 | # 276 | # logger.level = Logger::ERROR 277 | # logger.debug? # => false 278 | # logger.info? # => false 279 | # logger.warn? # => false 280 | # logger.error? # => true 281 | # logger.fatal? # => true 282 | # 283 | # == Log File Rotation 284 | # 285 | # By default, a log file is a single file that grows indefinitely 286 | # (until explicitly closed); there is no file rotation. 287 | # 288 | # To keep log files to a manageable size, 289 | # you can use _log_ _file_ _rotation_, which uses multiple log files: 290 | # 291 | # - Each log file has entries for a non-overlapping 292 | # time interval. 293 | # - Only the most recent log file is open and active; 294 | # the others are closed and inactive. 295 | # 296 | # === Size-Based Rotation 297 | # 298 | # For size-based log file rotation, call Logger.new with: 299 | # 300 | # - Argument +logdev+ as a file path. 301 | # - Argument +shift_age+ with a positive integer: 302 | # the number of log files to be in the rotation. 303 | # - Argument +shift_size+ as a positive integer: 304 | # the maximum size (in bytes) of each log file; 305 | # defaults to 1048576 (1 megabyte). 306 | # 307 | # Examples: 308 | # 309 | # logger = Logger.new('t.log', 3) # Three 1-megabyte files. 310 | # logger = Logger.new('t.log', 5, 10485760) # Five 10-megabyte files. 311 | # 312 | # For these examples, suppose: 313 | # 314 | # logger = Logger.new('t.log', 3) 315 | # 316 | # Logging begins in the new log file, +t.log+; 317 | # the log file is "full" and ready for rotation 318 | # when a new entry would cause its size to exceed +shift_size+. 319 | # 320 | # The first time +t.log+ is full: 321 | # 322 | # - +t.log+ is closed and renamed to +t.log.0+. 323 | # - A new file +t.log+ is opened. 324 | # 325 | # The second time +t.log+ is full: 326 | # 327 | # - +t.log.0 is renamed as +t.log.1+. 328 | # - +t.log+ is closed and renamed to +t.log.0+. 329 | # - A new file +t.log+ is opened. 330 | # 331 | # Each subsequent time that +t.log+ is full, 332 | # the log files are rotated: 333 | # 334 | # - +t.log.1+ is removed. 335 | # - +t.log.0 is renamed as +t.log.1+. 336 | # - +t.log+ is closed and renamed to +t.log.0+. 337 | # - A new file +t.log+ is opened. 338 | # 339 | # === Periodic Rotation 340 | # 341 | # For periodic rotation, call Logger.new with: 342 | # 343 | # - Argument +logdev+ as a file path. 344 | # - Argument +shift_age+ as a string period indicator. 345 | # 346 | # Examples: 347 | # 348 | # logger = Logger.new('t.log', 'daily') # Rotate log files daily. 349 | # logger = Logger.new('t.log', 'weekly') # Rotate log files weekly. 350 | # logger = Logger.new('t.log', 'monthly') # Rotate log files monthly. 351 | # 352 | # Example: 353 | # 354 | # logger = Logger.new('t.log', 'daily') 355 | # 356 | # When the given period expires: 357 | # 358 | # - The base log file, +t.log+ is closed and renamed 359 | # with a date-based suffix such as +t.log.20220509+. 360 | # - A new log file +t.log+ is opened. 361 | # - Nothing is removed. 362 | # 363 | # The default format for the suffix is '%Y%m%d', 364 | # which produces a suffix similar to the one above. 365 | # You can set a different format using create-time option 366 | # +shift_period_suffix+; 367 | # see details and suggestions at 368 | # {Time#strftime}[https://docs.ruby-lang.org/en/master/Time.html#method-i-strftime]. 369 | # 370 | class Logger 371 | _, name, rev = %w$Id$ 372 | if name 373 | name = name.chomp(",v") 374 | else 375 | name = File.basename(__FILE__) 376 | end 377 | rev ||= "v#{VERSION}" 378 | ProgName = "#{name}/#{rev}" 379 | 380 | include Severity 381 | 382 | # Logging severity threshold (e.g. Logger::INFO). 383 | def level 384 | level_override[level_key] || @level 385 | end 386 | 387 | # Sets the log level; returns +severity+. 388 | # See {Log Level}[rdoc-ref:Logger@Log+Level]. 389 | # 390 | # Argument +severity+ may be an integer, a string, or a symbol: 391 | # 392 | # logger.level = Logger::ERROR # => 3 393 | # logger.level = 3 # => 3 394 | # logger.level = 'error' # => "error" 395 | # logger.level = :error # => :error 396 | # 397 | # Logger#sev_threshold= is an alias for Logger#level=. 398 | # 399 | def level=(severity) 400 | @level = Severity.coerce(severity) 401 | end 402 | 403 | # Adjust the log level during the block execution for the current Fiber only 404 | # 405 | # logger.with_level(:debug) do 406 | # logger.debug { "Hello" } 407 | # end 408 | def with_level(severity) 409 | prev, level_override[level_key] = level, Severity.coerce(severity) 410 | begin 411 | yield 412 | ensure 413 | if prev 414 | level_override[level_key] = prev 415 | else 416 | level_override.delete(level_key) 417 | end 418 | end 419 | end 420 | 421 | # Program name to include in log messages. 422 | attr_accessor :progname 423 | 424 | # Sets the date-time format. 425 | # 426 | # Argument +datetime_format+ should be either of these: 427 | # 428 | # - A string suitable for use as a format for method 429 | # {Time#strftime}[https://docs.ruby-lang.org/en/master/Time.html#method-i-strftime]. 430 | # - +nil+: the logger uses '%Y-%m-%dT%H:%M:%S.%6N'. 431 | # 432 | def datetime_format=(datetime_format) 433 | @default_formatter.datetime_format = datetime_format 434 | end 435 | 436 | # Returns the date-time format; see #datetime_format=. 437 | # 438 | def datetime_format 439 | @default_formatter.datetime_format 440 | end 441 | 442 | # Sets or retrieves the logger entry formatter proc. 443 | # 444 | # When +formatter+ is +nil+, the logger uses Logger::Formatter. 445 | # 446 | # When +formatter+ is a proc, a new entry is formatted by the proc, 447 | # which is called with four arguments: 448 | # 449 | # - +severity+: The severity of the entry. 450 | # - +time+: A Time object representing the entry's timestamp. 451 | # - +progname+: The program name for the entry. 452 | # - +msg+: The message for the entry (string or string-convertible object). 453 | # 454 | # The proc should return a string containing the formatted entry. 455 | # 456 | # This custom formatter uses 457 | # {String#dump}[https://docs.ruby-lang.org/en/master/String.html#method-i-dump] 458 | # to escape the message string: 459 | # 460 | # logger = Logger.new($stdout, progname: 'mung') 461 | # original_formatter = logger.formatter || Logger::Formatter.new 462 | # logger.formatter = proc { |severity, time, progname, msg| 463 | # original_formatter.call(severity, time, progname, msg.dump) 464 | # } 465 | # logger.add(Logger::INFO, "hello \n ''") 466 | # logger.add(Logger::INFO, "\f\x00\xff\\\"") 467 | # 468 | # Output: 469 | # 470 | # I, [2022-05-13T13:16:29.637488 #8492] INFO -- mung: "hello \n ''" 471 | # I, [2022-05-13T13:16:29.637610 #8492] INFO -- mung: "\f\x00\xFF\\\"" 472 | # 473 | attr_accessor :formatter 474 | 475 | alias sev_threshold level 476 | alias sev_threshold= level= 477 | 478 | # Returns +true+ if the log level allows entries with severity 479 | # Logger::DEBUG to be written, +false+ otherwise. 480 | # See {Log Level}[rdoc-ref:Logger@Log+Level]. 481 | # 482 | def debug?; level <= DEBUG; end 483 | 484 | # Sets the log level to Logger::DEBUG. 485 | # See {Log Level}[rdoc-ref:Logger@Log+Level]. 486 | # 487 | def debug!; self.level = DEBUG; end 488 | 489 | # Returns +true+ if the log level allows entries with severity 490 | # Logger::INFO to be written, +false+ otherwise. 491 | # See {Log Level}[rdoc-ref:Logger@Log+Level]. 492 | # 493 | def info?; level <= INFO; end 494 | 495 | # Sets the log level to Logger::INFO. 496 | # See {Log Level}[rdoc-ref:Logger@Log+Level]. 497 | # 498 | def info!; self.level = INFO; end 499 | 500 | # Returns +true+ if the log level allows entries with severity 501 | # Logger::WARN to be written, +false+ otherwise. 502 | # See {Log Level}[rdoc-ref:Logger@Log+Level]. 503 | # 504 | def warn?; level <= WARN; end 505 | 506 | # Sets the log level to Logger::WARN. 507 | # See {Log Level}[rdoc-ref:Logger@Log+Level]. 508 | # 509 | def warn!; self.level = WARN; end 510 | 511 | # Returns +true+ if the log level allows entries with severity 512 | # Logger::ERROR to be written, +false+ otherwise. 513 | # See {Log Level}[rdoc-ref:Logger@Log+Level]. 514 | # 515 | def error?; level <= ERROR; end 516 | 517 | # Sets the log level to Logger::ERROR. 518 | # See {Log Level}[rdoc-ref:Logger@Log+Level]. 519 | # 520 | def error!; self.level = ERROR; end 521 | 522 | # Returns +true+ if the log level allows entries with severity 523 | # Logger::FATAL to be written, +false+ otherwise. 524 | # See {Log Level}[rdoc-ref:Logger@Log+Level]. 525 | # 526 | def fatal?; level <= FATAL; end 527 | 528 | # Sets the log level to Logger::FATAL. 529 | # See {Log Level}[rdoc-ref:Logger@Log+Level]. 530 | # 531 | def fatal!; self.level = FATAL; end 532 | 533 | # :call-seq: 534 | # Logger.new(logdev, shift_age = 0, shift_size = 1048576, **options) 535 | # 536 | # With the single argument +logdev+, 537 | # returns a new logger with all default options: 538 | # 539 | # Logger.new('t.log') # => # 540 | # 541 | # Argument +logdev+ must be one of: 542 | # 543 | # - A string filepath: entries are to be written 544 | # to the file at that path; if the file at that path exists, 545 | # new entries are appended. 546 | # - An IO stream (typically $stdout, $stderr. or 547 | # an open file): entries are to be written to the given stream. 548 | # - An instance of Logger::LogDevice, such as the #logdev of another Logger. 549 | # - +nil+ or +File::NULL+: no entries are to be written. 550 | # 551 | # Argument +shift_age+ must be one of: 552 | # 553 | # - The number of log files to be in the rotation. 554 | # See {Size-Based Rotation}[rdoc-ref:Logger@Size-Based+Rotation]. 555 | # - A string period indicator. 556 | # See {Periodic Rotation}[rdoc-ref:Logger@Periodic+Rotation]. 557 | # 558 | # Argument +shift_size+ is the maximum size (in bytes) of each log file. 559 | # See {Size-Based Rotation}[rdoc-ref:Logger@Size-Based+Rotation]. 560 | # 561 | # Examples: 562 | # 563 | # Logger.new('t.log') 564 | # Logger.new($stdout) 565 | # 566 | # The keyword options are: 567 | # 568 | # - +level+: sets the log level; default value is Logger::DEBUG. 569 | # See {Log Level}[rdoc-ref:Logger@Log+Level]: 570 | # 571 | # Logger.new('t.log', level: Logger::ERROR) 572 | # 573 | # - +progname+: sets the default program name; default is +nil+. 574 | # See {Program Name}[rdoc-ref:Logger@Program+Name]: 575 | # 576 | # Logger.new('t.log', progname: 'mung') 577 | # 578 | # - +formatter+: sets the entry formatter; default is +nil+. 579 | # See {formatter=}[Logger.html#attribute-i-formatter]. 580 | # 581 | # - +datetime_format+: sets the format for entry timestamp; 582 | # default is +nil+. 583 | # See #datetime_format=. 584 | # 585 | # - +binmode+: sets whether the logger writes in binary mode; 586 | # default is +false+. 587 | # 588 | # - +shift_period_suffix+: sets the format for the filename suffix 589 | # for periodic log file rotation; default is '%Y%m%d'. 590 | # See {Periodic Rotation}[rdoc-ref:Logger@Periodic+Rotation]. 591 | # 592 | # - +reraise_write_errors+: An array of exception classes, which will 593 | # be reraised if there is an error when writing to the log device. 594 | # The default is to swallow all exceptions raised. 595 | # - +skip_header+: If +true+, prevents the logger from writing a header 596 | # when creating a new log file. The default is +false+, meaning 597 | # the header will be written as usual. 598 | # 599 | def initialize(logdev, shift_age = 0, shift_size = 1048576, level: DEBUG, 600 | progname: nil, formatter: nil, datetime_format: nil, 601 | binmode: false, shift_period_suffix: '%Y%m%d', 602 | reraise_write_errors: [], skip_header: false) 603 | self.level = level 604 | self.progname = progname 605 | @default_formatter = Formatter.new 606 | self.datetime_format = datetime_format 607 | self.formatter = formatter 608 | @logdev = nil 609 | @level_override = {} 610 | return unless logdev 611 | case logdev 612 | when File::NULL 613 | # null logger 614 | when LogDevice 615 | @logdev = logdev 616 | else 617 | @logdev = LogDevice.new(logdev, shift_age: shift_age, 618 | shift_size: shift_size, 619 | shift_period_suffix: shift_period_suffix, 620 | binmode: binmode, 621 | reraise_write_errors: reraise_write_errors, 622 | skip_header: skip_header) 623 | end 624 | end 625 | 626 | # The underlying log device. 627 | # 628 | # This is the first argument passed to the constructor, wrapped in a 629 | # Logger::LogDevice, along with the binmode flag and rotation options. 630 | attr_reader :logdev 631 | 632 | # Sets the logger's output stream: 633 | # 634 | # - If +logdev+ is +nil+, reopens the current output stream. 635 | # - If +logdev+ is a filepath, opens the indicated file for append. 636 | # - If +logdev+ is an IO stream 637 | # (usually $stdout, $stderr, or an open File object), 638 | # opens the stream for append. 639 | # 640 | # Example: 641 | # 642 | # logger = Logger.new('t.log') 643 | # logger.add(Logger::ERROR, 'one') 644 | # logger.close 645 | # logger.add(Logger::ERROR, 'two') # Prints 'log writing failed. closed stream' 646 | # logger.reopen 647 | # logger.add(Logger::ERROR, 'three') 648 | # logger.close 649 | # File.readlines('t.log') 650 | # # => 651 | # # ["# Logfile created on 2022-05-12 14:21:19 -0500 by logger.rb/v1.5.0\n", 652 | # # "E, [2022-05-12T14:21:27.596726 #22428] ERROR -- : one\n", 653 | # # "E, [2022-05-12T14:23:05.847241 #22428] ERROR -- : three\n"] 654 | # 655 | def reopen(logdev = nil, shift_age = nil, shift_size = nil, shift_period_suffix: nil, binmode: nil) 656 | @logdev&.reopen(logdev, shift_age: shift_age, shift_size: shift_size, 657 | shift_period_suffix: shift_period_suffix, binmode: binmode) 658 | self 659 | end 660 | 661 | # Creates a log entry, which may or may not be written to the log, 662 | # depending on the entry's severity and on the log level. 663 | # See {Log Level}[rdoc-ref:Logger@Log+Level] 664 | # and {Entries}[rdoc-ref:Logger@Entries] for details. 665 | # 666 | # Examples: 667 | # 668 | # logger = Logger.new($stdout, progname: 'mung') 669 | # logger.add(Logger::INFO) 670 | # logger.add(Logger::ERROR, 'No good') 671 | # logger.add(Logger::ERROR, 'No good', 'gnum') 672 | # 673 | # Output: 674 | # 675 | # I, [2022-05-12T16:25:31.469726 #36328] INFO -- mung: mung 676 | # E, [2022-05-12T16:25:55.349414 #36328] ERROR -- mung: No good 677 | # E, [2022-05-12T16:26:35.841134 #36328] ERROR -- gnum: No good 678 | # 679 | # These convenience methods have implicit severity: 680 | # 681 | # - #debug. 682 | # - #info. 683 | # - #warn. 684 | # - #error. 685 | # - #fatal. 686 | # - #unknown. 687 | # 688 | def add(severity, message = nil, progname = nil) 689 | severity ||= UNKNOWN 690 | if @logdev.nil? or severity < level 691 | return true 692 | end 693 | if progname.nil? 694 | progname = @progname 695 | end 696 | if message.nil? 697 | if block_given? 698 | message = yield 699 | else 700 | message = progname 701 | progname = @progname 702 | end 703 | end 704 | @logdev.write( 705 | format_message(format_severity(severity), Time.now, progname, message)) 706 | true 707 | end 708 | alias log add 709 | 710 | # Writes the given +msg+ to the log with no formatting; 711 | # returns the number of characters written, 712 | # or +nil+ if no log device exists: 713 | # 714 | # logger = Logger.new($stdout) 715 | # logger << 'My message.' # => 10 716 | # 717 | # Output: 718 | # 719 | # My message. 720 | # 721 | def <<(msg) 722 | @logdev&.write(msg) 723 | end 724 | 725 | # Equivalent to calling #add with severity Logger::DEBUG. 726 | # 727 | def debug(progname = nil, &block) 728 | add(DEBUG, nil, progname, &block) 729 | end 730 | 731 | # Equivalent to calling #add with severity Logger::INFO. 732 | # 733 | def info(progname = nil, &block) 734 | add(INFO, nil, progname, &block) 735 | end 736 | 737 | # Equivalent to calling #add with severity Logger::WARN. 738 | # 739 | def warn(progname = nil, &block) 740 | add(WARN, nil, progname, &block) 741 | end 742 | 743 | # Equivalent to calling #add with severity Logger::ERROR. 744 | # 745 | def error(progname = nil, &block) 746 | add(ERROR, nil, progname, &block) 747 | end 748 | 749 | # Equivalent to calling #add with severity Logger::FATAL. 750 | # 751 | def fatal(progname = nil, &block) 752 | add(FATAL, nil, progname, &block) 753 | end 754 | 755 | # Equivalent to calling #add with severity Logger::UNKNOWN. 756 | # 757 | def unknown(progname = nil, &block) 758 | add(UNKNOWN, nil, progname, &block) 759 | end 760 | 761 | # Closes the logger; returns +nil+: 762 | # 763 | # logger = Logger.new('t.log') 764 | # logger.close # => nil 765 | # logger.info('foo') # Prints "log writing failed. closed stream" 766 | # 767 | # Related: Logger#reopen. 768 | def close 769 | @logdev&.close 770 | end 771 | 772 | private 773 | 774 | # \Severity label for logging (max 5 chars). 775 | SEV_LABEL = %w(DEBUG INFO WARN ERROR FATAL ANY).freeze 776 | 777 | def format_severity(severity) 778 | SEV_LABEL[severity] || 'ANY' 779 | end 780 | 781 | # Guarantee the existence of this ivar even when subclasses don't call the superclass constructor. 782 | def level_override 783 | unless defined?(@level_override) 784 | bad = self.class.instance_method(:initialize) 785 | file, line = bad.source_location 786 | Kernel.warn <<~";;;", uplevel: 2 787 | Logger not initialized properly 788 | #{file}:#{line}: info: #{bad.owner}\##{bad.name}: \ 789 | does not call super probably 790 | ;;; 791 | end 792 | @level_override ||= {} 793 | end 794 | 795 | def level_key 796 | Fiber.current 797 | end 798 | 799 | def format_message(severity, datetime, progname, msg) 800 | (@formatter || @default_formatter).call(severity, datetime, progname, msg) 801 | end 802 | end 803 | -------------------------------------------------------------------------------- /test/logger/test_logdevice.rb: -------------------------------------------------------------------------------- 1 | # coding: US-ASCII 2 | # frozen_string_literal: false 3 | require 'logger' 4 | require 'tempfile' 5 | require 'tmpdir' 6 | require 'pathname' 7 | 8 | class TestLogDevice < Test::Unit::TestCase 9 | class LogExcnRaiser 10 | def write(*arg) 11 | raise 'disk is full' 12 | end 13 | 14 | def close 15 | end 16 | 17 | def stat 18 | Object.new 19 | end 20 | end 21 | 22 | def setup 23 | @top_dir = File.expand_path('../../lib', __dir__) 24 | @tempfile = Tempfile.new("logger") 25 | @tempfile.close 26 | @filename = @tempfile.path 27 | File.unlink(@filename) 28 | end 29 | 30 | def teardown 31 | @tempfile.close(true) 32 | end 33 | 34 | def d(log, opt = {}) 35 | Logger::LogDevice.new(log, **opt) 36 | end 37 | 38 | def test_initialize 39 | logdev = d(STDERR) 40 | assert_equal(STDERR, logdev.dev) 41 | assert_nil(logdev.filename) 42 | assert_raise(TypeError) do 43 | d(nil) 44 | end 45 | # 46 | logdev = d(@filename) 47 | begin 48 | assert_file.exist?(@filename) 49 | assert_predicate(logdev.dev, :sync) 50 | refute_predicate(logdev.dev, :binmode?) 51 | assert_equal(@filename, logdev.filename) 52 | logdev.write('hello') 53 | ensure 54 | logdev.close 55 | end 56 | # create logfile which is already exist. 57 | logdev = d(@filename) 58 | begin 59 | assert_predicate(logdev.dev, :sync) 60 | refute_predicate(logdev.dev, :binmode?) 61 | logdev.write('world') 62 | logfile = File.read(@filename) 63 | assert_equal(2, logfile.split(/\n/).size) 64 | assert_match(/^helloworld$/, logfile) 65 | ensure 66 | logdev.close 67 | end 68 | # logfile object with path 69 | tempfile = Tempfile.new("logger") 70 | tempfile.sync = true 71 | logdev = d(tempfile) 72 | begin 73 | logdev.write('world') 74 | logfile = File.read(tempfile.path) 75 | assert_equal(1, logfile.split(/\n/).size) 76 | assert_match(/^world$/, logfile) 77 | assert_equal(tempfile.path, logdev.filename) 78 | ensure 79 | logdev.close 80 | File.unlink(tempfile) 81 | tempfile.close(true) 82 | end 83 | # logfile object with Pathname object 84 | tempfile = Tempfile.new("logger") 85 | pathname = Pathname.new(tempfile.path) 86 | logdev = d(pathname) 87 | begin 88 | logdev.write('world') 89 | logfile = File.read(pathname) 90 | assert_equal(1, logfile.split(/\n/).size) 91 | assert_match(/^world$/, logfile) 92 | assert_equal(pathname, logdev.filename) 93 | ensure 94 | logdev.close 95 | tempfile.close(true) 96 | end 97 | end 98 | 99 | def test_write 100 | r, w = IO.pipe 101 | logdev = d(w) 102 | logdev.write("msg2\n\n") 103 | IO.select([r], nil, nil, 0.1) 104 | w.close 105 | msg = r.read 106 | r.close 107 | assert_equal("msg2\n\n", msg) 108 | # 109 | logdev = d(LogExcnRaiser.new) 110 | class << (stderr = '') 111 | alias write concat 112 | end 113 | $stderr, stderr = stderr, $stderr 114 | begin 115 | assert_nothing_raised do 116 | logdev.write('hello') 117 | end 118 | ensure 119 | logdev.close 120 | $stderr, stderr = stderr, $stderr 121 | end 122 | assert_equal "log writing failed. disk is full\n", stderr 123 | end 124 | 125 | def test_close 126 | r, w = IO.pipe 127 | logdev = d(w) 128 | logdev.write("msg2\n\n") 129 | IO.select([r], nil, nil, 0.1) 130 | assert_not_predicate(w, :closed?) 131 | logdev.close 132 | assert_predicate(w, :closed?) 133 | r.close 134 | end 135 | 136 | def test_reopen_io 137 | logdev = d(STDERR) 138 | old_dev = logdev.dev 139 | logdev.reopen 140 | assert_equal(STDERR, logdev.dev) 141 | assert_not_predicate(old_dev, :closed?) 142 | end 143 | 144 | def test_reopen_io_by_io 145 | logdev = d(STDERR) 146 | old_dev = logdev.dev 147 | logdev.reopen(STDOUT) 148 | assert_equal(STDOUT, logdev.dev) 149 | assert_not_predicate(old_dev, :closed?) 150 | end 151 | 152 | def test_reopen_io_by_file 153 | logdev = d(STDERR) 154 | old_dev = logdev.dev 155 | logdev.reopen(@filename) 156 | begin 157 | assert_file.exist?(@filename) 158 | assert_equal(@filename, logdev.filename) 159 | assert_not_predicate(old_dev, :closed?) 160 | ensure 161 | logdev.close 162 | end 163 | end 164 | 165 | def test_reopen_file 166 | logdev = d(@filename) 167 | old_dev = logdev.dev 168 | 169 | logdev.reopen 170 | begin 171 | assert_file.exist?(@filename) 172 | assert_equal(@filename, logdev.filename) 173 | assert_predicate(old_dev, :closed?) 174 | ensure 175 | logdev.close 176 | end 177 | end 178 | 179 | def test_reopen_file_by_io 180 | logdev = d(@filename) 181 | old_dev = logdev.dev 182 | logdev.reopen(STDOUT) 183 | assert_equal(STDOUT, logdev.dev) 184 | assert_nil(logdev.filename) 185 | assert_predicate(old_dev, :closed?) 186 | end 187 | 188 | def test_reopen_file_by_file 189 | logdev = d(@filename) 190 | old_dev = logdev.dev 191 | 192 | tempfile2 = Tempfile.new("logger") 193 | tempfile2.close 194 | filename2 = tempfile2.path 195 | File.unlink(filename2) 196 | 197 | logdev.reopen(filename2) 198 | begin 199 | assert_file.exist?(filename2) 200 | assert_equal(filename2, logdev.filename) 201 | assert_predicate(old_dev, :closed?) 202 | ensure 203 | logdev.close 204 | tempfile2.close(true) 205 | end 206 | end 207 | 208 | def test_perm_after_shift 209 | mode = 0o611 210 | File.open(@filename, "w") {|f| f.chmod mode} 211 | logfile = @filename 212 | logfile0 = logfile + '.0' 213 | logdev = d(@filename, shift_age: 1, shift_size: 0) 214 | logdev.write('hello') 215 | logdev.write('hello') 216 | logdev.close 217 | 218 | assert_equal File.stat(logfile0).mode, File.stat(logfile).mode 219 | ensure 220 | if logfile0 221 | File.unlink(logfile0) rescue nil 222 | end 223 | end 224 | 225 | def test_shifting_size_with_reopen 226 | tmpfile = Tempfile.new([File.basename(__FILE__, '.*'), '_1.log']) 227 | logfile = tmpfile.path 228 | logfile0 = logfile + '.0' 229 | logfile1 = logfile + '.1' 230 | logfile2 = logfile + '.2' 231 | logfile3 = logfile + '.3' 232 | tmpfile.close(true) 233 | File.unlink(logfile) if File.exist?(logfile) 234 | File.unlink(logfile0) if File.exist?(logfile0) 235 | File.unlink(logfile1) if File.exist?(logfile1) 236 | File.unlink(logfile2) if File.exist?(logfile2) 237 | 238 | logger = Logger.new(STDERR) 239 | logger.reopen(logfile, 4, 100) 240 | 241 | logger.error("0" * 15) 242 | assert_file.exist?(logfile) 243 | assert_file.not_exist?(logfile0) 244 | logger.error("0" * 15) 245 | assert_file.exist?(logfile0) 246 | assert_file.not_exist?(logfile1) 247 | logger.error("0" * 15) 248 | assert_file.exist?(logfile1) 249 | assert_file.not_exist?(logfile2) 250 | logger.error("0" * 15) 251 | assert_file.exist?(logfile2) 252 | assert_file.not_exist?(logfile3) 253 | logger.error("0" * 15) 254 | assert_file.not_exist?(logfile3) 255 | logger.error("0" * 15) 256 | assert_file.not_exist?(logfile3) 257 | logger.close 258 | File.unlink(logfile) 259 | File.unlink(logfile0) 260 | File.unlink(logfile1) 261 | File.unlink(logfile2) 262 | 263 | tmpfile = Tempfile.new([File.basename(__FILE__, '.*'), '_2.log']) 264 | logfile = tmpfile.path 265 | logfile0 = logfile + '.0' 266 | logfile1 = logfile + '.1' 267 | logfile2 = logfile + '.2' 268 | logfile3 = logfile + '.3' 269 | tmpfile.close(true) 270 | logger = Logger.new(logfile, 4, 150) 271 | logger.error("0" * 15) 272 | assert_file.exist?(logfile) 273 | assert_file.not_exist?(logfile0) 274 | logger.error("0" * 15) 275 | assert_file.not_exist?(logfile0) 276 | logger.error("0" * 15) 277 | assert_file.exist?(logfile0) 278 | assert_file.not_exist?(logfile1) 279 | logger.error("0" * 15) 280 | assert_file.not_exist?(logfile1) 281 | logger.error("0" * 15) 282 | assert_file.exist?(logfile1) 283 | assert_file.not_exist?(logfile2) 284 | logger.error("0" * 15) 285 | assert_file.not_exist?(logfile2) 286 | logger.error("0" * 15) 287 | assert_file.exist?(logfile2) 288 | assert_file.not_exist?(logfile3) 289 | logger.error("0" * 15) 290 | assert_file.not_exist?(logfile3) 291 | logger.error("0" * 15) 292 | assert_file.not_exist?(logfile3) 293 | logger.error("0" * 15) 294 | assert_file.not_exist?(logfile3) 295 | logger.close 296 | File.unlink(logfile) 297 | File.unlink(logfile0) 298 | File.unlink(logfile1) 299 | File.unlink(logfile2) 300 | end 301 | 302 | def test_shifting_size 303 | tmpfile = Tempfile.new([File.basename(__FILE__, '.*'), '_1.log']) 304 | logfile = tmpfile.path 305 | logfile0 = logfile + '.0' 306 | logfile1 = logfile + '.1' 307 | logfile2 = logfile + '.2' 308 | logfile3 = logfile + '.3' 309 | tmpfile.close(true) 310 | File.unlink(logfile) if File.exist?(logfile) 311 | File.unlink(logfile0) if File.exist?(logfile0) 312 | File.unlink(logfile1) if File.exist?(logfile1) 313 | File.unlink(logfile2) if File.exist?(logfile2) 314 | logger = Logger.new(logfile, 4, 100) 315 | logger.error("0" * 15) 316 | assert_file.exist?(logfile) 317 | assert_file.not_exist?(logfile0) 318 | logger.error("0" * 15) 319 | assert_file.exist?(logfile0) 320 | assert_file.not_exist?(logfile1) 321 | logger.error("0" * 15) 322 | assert_file.exist?(logfile1) 323 | assert_file.not_exist?(logfile2) 324 | logger.error("0" * 15) 325 | assert_file.exist?(logfile2) 326 | assert_file.not_exist?(logfile3) 327 | logger.error("0" * 15) 328 | assert_file.not_exist?(logfile3) 329 | logger.error("0" * 15) 330 | assert_file.not_exist?(logfile3) 331 | logger.close 332 | File.unlink(logfile) 333 | File.unlink(logfile0) 334 | File.unlink(logfile1) 335 | File.unlink(logfile2) 336 | 337 | tmpfile = Tempfile.new([File.basename(__FILE__, '.*'), '_2.log']) 338 | logfile = tmpfile.path 339 | logfile0 = logfile + '.0' 340 | logfile1 = logfile + '.1' 341 | logfile2 = logfile + '.2' 342 | logfile3 = logfile + '.3' 343 | tmpfile.close(true) 344 | logger = Logger.new(logfile, 4, 150) 345 | logger.error("0" * 15) 346 | assert_file.exist?(logfile) 347 | assert_file.not_exist?(logfile0) 348 | logger.error("0" * 15) 349 | assert_file.not_exist?(logfile0) 350 | logger.error("0" * 15) 351 | assert_file.exist?(logfile0) 352 | assert_file.not_exist?(logfile1) 353 | logger.error("0" * 15) 354 | assert_file.not_exist?(logfile1) 355 | logger.error("0" * 15) 356 | assert_file.exist?(logfile1) 357 | assert_file.not_exist?(logfile2) 358 | logger.error("0" * 15) 359 | assert_file.not_exist?(logfile2) 360 | logger.error("0" * 15) 361 | assert_file.exist?(logfile2) 362 | assert_file.not_exist?(logfile3) 363 | logger.error("0" * 15) 364 | assert_file.not_exist?(logfile3) 365 | logger.error("0" * 15) 366 | assert_file.not_exist?(logfile3) 367 | logger.error("0" * 15) 368 | assert_file.not_exist?(logfile3) 369 | logger.close 370 | File.unlink(logfile) 371 | File.unlink(logfile0) 372 | File.unlink(logfile1) 373 | File.unlink(logfile2) 374 | end 375 | 376 | def test_shifting_age_variants 377 | logger = Logger.new(@filename, 'daily') 378 | logger.info('daily') 379 | logger.close 380 | logger = Logger.new(@filename, 'weekly') 381 | logger.info('weekly') 382 | logger.close 383 | logger = Logger.new(@filename, 'monthly') 384 | logger.info('monthly') 385 | logger.close 386 | end 387 | 388 | def test_invalid_shifting_age 389 | assert_raise(ArgumentError) { Logger::Period.next_rotate_time(Time.now, 'invalid') } 390 | assert_raise(ArgumentError) { Logger::Period.previous_period_end(Time.now, 'invalid') } 391 | end 392 | 393 | def test_shifting_age 394 | yyyymmdd = Time.now.strftime("%Y%m%d") 395 | filename1 = @filename + ".#{yyyymmdd}" 396 | filename2 = @filename + ".#{yyyymmdd}.1" 397 | filename3 = @filename + ".#{yyyymmdd}.2" 398 | begin 399 | logger = Logger.new(@filename, 'now') 400 | assert_file.exist?(@filename) 401 | assert_file.not_exist?(filename1) 402 | assert_file.not_exist?(filename2) 403 | assert_file.not_exist?(filename3) 404 | logger.info("0" * 15) 405 | assert_file.exist?(@filename) 406 | assert_file.exist?(filename1) 407 | assert_file.not_exist?(filename2) 408 | assert_file.not_exist?(filename3) 409 | logger.warn("0" * 15) 410 | assert_file.exist?(@filename) 411 | assert_file.exist?(filename1) 412 | assert_file.exist?(filename2) 413 | assert_file.not_exist?(filename3) 414 | logger.error("0" * 15) 415 | assert_file.exist?(@filename) 416 | assert_file.exist?(filename1) 417 | assert_file.exist?(filename2) 418 | assert_file.exist?(filename3) 419 | ensure 420 | logger.close if logger 421 | [filename1, filename2, filename3].each do |filename| 422 | File.unlink(filename) if File.exist?(filename) 423 | end 424 | end 425 | end 426 | 427 | def test_shifting_period_suffix 428 | ['%Y%m%d', '%Y-%m-%d', '%Y'].each do |format| 429 | if format == '%Y%m%d' # default 430 | logger = Logger.new(@filename, 'now', 1048576) 431 | else # config 432 | logger = Logger.new(@filename, 'now', 1048576, shift_period_suffix: format) 433 | end 434 | begin 435 | yyyymmdd = Time.now.strftime(format) 436 | filename1 = @filename + ".#{yyyymmdd}" 437 | filename2 = @filename + ".#{yyyymmdd}.1" 438 | filename3 = @filename + ".#{yyyymmdd}.2" 439 | logger.info("0" * 15) 440 | logger.info("0" * 15) 441 | logger.info("0" * 15) 442 | assert_file.exist?(@filename) 443 | assert_file.exist?(filename1) 444 | assert_file.exist?(filename2) 445 | assert_file.exist?(filename3) 446 | ensure 447 | logger.close if logger 448 | [filename1, filename2, filename3].each do |filename| 449 | File.unlink(filename) if File.exist?(filename) 450 | end 451 | end 452 | end 453 | end 454 | 455 | def test_shifting_size_in_multiprocess 456 | tmpfile = Tempfile.new([File.basename(__FILE__, '.*'), '_1.log']) 457 | logfile = tmpfile.path 458 | logfile0 = logfile + '.0' 459 | logfile1 = logfile + '.1' 460 | logfile2 = logfile + '.2' 461 | tmpfile.close(true) 462 | File.unlink(logfile) if File.exist?(logfile) 463 | File.unlink(logfile0) if File.exist?(logfile0) 464 | File.unlink(logfile1) if File.exist?(logfile1) 465 | File.unlink(logfile2) if File.exist?(logfile2) 466 | begin 467 | stderr = run_children(2, [logfile], "#{<<-"begin;"}\n#{<<-'end;'}") 468 | begin; 469 | logger = Logger.new(ARGV[0], 4, 10) 470 | 10.times do 471 | logger.info '0' * 15 472 | end 473 | end; 474 | assert_no_match(/log shifting failed/, stderr) 475 | assert_no_match(/log writing failed/, stderr) 476 | assert_no_match(/log rotation inter-process lock failed/, stderr) 477 | ensure 478 | File.unlink(logfile) if File.exist?(logfile) 479 | File.unlink(logfile0) if File.exist?(logfile0) 480 | File.unlink(logfile1) if File.exist?(logfile1) 481 | File.unlink(logfile2) if File.exist?(logfile2) 482 | end 483 | end 484 | 485 | def test_shifting_age_in_multiprocess 486 | yyyymmdd = Time.now.strftime("%Y%m%d") 487 | begin 488 | stderr = run_children(2, [@filename], "#{<<-"begin;"}\n#{<<-'end;'}") 489 | begin; 490 | logger = Logger.new(ARGV[0], 'now') 491 | 10.times do 492 | logger.info '0' * 15 493 | end 494 | end; 495 | assert_no_match(/log shifting failed/, stderr) 496 | assert_no_match(/log writing failed/, stderr) 497 | assert_no_match(/log rotation inter-process lock failed/, stderr) 498 | ensure 499 | Dir.glob("#{@filename}.#{yyyymmdd}{,.[1-9]*}") do |filename| 500 | File.unlink(filename) if File.exist?(filename) 501 | end 502 | end 503 | end 504 | 505 | def test_open_without_header 506 | d(@filename, skip_header: true) 507 | 508 | assert_equal("", File.read(@filename)) 509 | end 510 | 511 | def test_open_logfile_in_multiprocess 512 | tmpfile = Tempfile.new([File.basename(__FILE__, '.*'), '_1.log']) 513 | logfile = tmpfile.path 514 | tmpfile.close(true) 515 | begin 516 | 20.times do 517 | run_children(2, [logfile], "#{<<-"begin;"}\n#{<<-'end;'}") 518 | begin; 519 | logfile = ARGV[0] 520 | logdev = Logger::LogDevice.new(logfile) 521 | logdev.send(:open_logfile, logfile) 522 | end; 523 | assert_equal(1, File.readlines(logfile).grep(/# Logfile created on/).size) 524 | File.unlink(logfile) 525 | end 526 | ensure 527 | File.unlink(logfile) if File.exist?(logfile) 528 | end 529 | end 530 | 531 | def test_shifting_size_not_rotate_too_much 532 | logdev0 = d(@filename) 533 | logdev0.__send__(:add_log_header, @tempfile) 534 | header_size = @tempfile.size 535 | message = "*" * 99 + "\n" 536 | shift_size = header_size + message.size * 3 - 1 537 | opt = {shift_age: 1, shift_size: shift_size} 538 | 539 | Dir.mktmpdir do |tmpdir| 540 | begin 541 | log = File.join(tmpdir, "log") 542 | logdev1 = d(log, opt) 543 | logdev2 = d(log, opt) 544 | 545 | assert_file.identical?(log, logdev1.dev) 546 | assert_file.identical?(log, logdev2.dev) 547 | 548 | 3.times{logdev1.write(message)} 549 | assert_file.identical?(log, logdev1.dev) 550 | assert_file.identical?(log, logdev2.dev) 551 | 552 | logdev1.write(message) 553 | assert_file.identical?(log, logdev1.dev) 554 | # NOTE: below assertion fails in JRuby 9.3 and TruffleRuby 555 | unless %w[jruby truffleruby].include? RUBY_ENGINE 556 | assert_file.identical?(log + ".0", logdev2.dev) 557 | end 558 | 559 | logdev2.write(message) 560 | assert_file.identical?(log, logdev1.dev) 561 | assert_file.identical?(log, logdev2.dev) 562 | 563 | logdev1.write(message) 564 | assert_file.identical?(log, logdev1.dev) 565 | assert_file.identical?(log, logdev2.dev) 566 | ensure 567 | logdev1.close if logdev1 568 | logdev2.close if logdev2 569 | end 570 | end 571 | ensure 572 | logdev0.close 573 | end 574 | 575 | def test_shifting_midnight 576 | Dir.mktmpdir do |tmpdir| 577 | assert_in_out_err([*%W"--disable=gems -I#{@top_dir} -rlogger -C#{tmpdir} -"], "#{<<-"begin;"}\n#{<<-'end;'}") 578 | begin; 579 | begin 580 | module FakeTime 581 | attr_accessor :now 582 | end 583 | 584 | class << Time 585 | prepend FakeTime 586 | end 587 | 588 | log = "log" 589 | File.open(log, "w") {} 590 | File.utime(*[Time.mktime(2014, 1, 2, 0, 0, 0)]*2, log) 591 | 592 | Time.now = Time.mktime(2014, 1, 2, 23, 59, 59, 999000) 593 | dev = Logger::LogDevice.new(log, shift_age: 'daily') 594 | dev.write("#{Time.now} hello-1\n") 595 | File.utime(Time.now, Time.now, log) 596 | 597 | Time.now = Time.mktime(2014, 1, 3, 1, 1, 1) 598 | dev.write("#{Time.now} hello-2\n") 599 | File.utime(Time.now, Time.now, log) 600 | ensure 601 | dev.close 602 | end 603 | end; 604 | 605 | bug = '[GH-539]' 606 | log = File.join(tmpdir, "log") 607 | cont = File.read(log) 608 | assert_match(/hello-2/, cont) 609 | assert_not_match(/hello-1/, cont) 610 | assert_file.for(bug).exist?(log+".20140102") 611 | assert_match(/hello-1/, File.read(log+".20140102"), bug) 612 | end 613 | end 614 | 615 | env_tz_works = /linux|darwin|freebsd|openbsd/ =~ RUBY_PLATFORM # borrow from test/ruby/test_time_tz.rb 616 | 617 | def test_shifting_weekly 618 | Dir.mktmpdir do |tmpdir| 619 | assert_in_out_err([{"TZ"=>"UTC"}, *%W"-I#{@top_dir} -rlogger -C#{tmpdir} -"], "#{<<-"begin;"}\n#{<<-'end;'}") 620 | begin; 621 | begin 622 | module FakeTime 623 | attr_accessor :now 624 | end 625 | 626 | class << Time 627 | prepend FakeTime 628 | end 629 | 630 | log = "log" 631 | File.open(log, "w") {} 632 | Time.now = Time.utc(2015, 12, 14, 0, 1, 1) 633 | File.utime(Time.now, Time.now, log) 634 | 635 | dev = Logger::LogDevice.new("log", shift_age: 'weekly') 636 | 637 | Time.now = Time.utc(2015, 12, 19, 12, 34, 56) 638 | dev.write("#{Time.now} hello-1\n") 639 | File.utime(Time.now, Time.now, log) 640 | 641 | Time.now = Time.utc(2015, 12, 20, 0, 1, 1) 642 | dev.write("#{Time.now} hello-2\n") 643 | File.utime(Time.now, Time.now, log) 644 | ensure 645 | dev.close if dev 646 | end 647 | end; 648 | log = File.join(tmpdir, "log") 649 | cont = File.read(log) 650 | assert_match(/hello-2/, cont) 651 | assert_not_match(/hello-1/, cont) 652 | log = Dir.glob(log+".*") 653 | assert_equal(1, log.size) 654 | log, = *log 655 | cont = File.read(log) 656 | assert_match(/hello-1/, cont) 657 | assert_equal("2015-12-19", cont[/^[-\d]+/]) 658 | assert_equal("20151219", log[/\d+\z/]) 659 | end 660 | end if env_tz_works 661 | 662 | def test_shifting_monthly 663 | Dir.mktmpdir do |tmpdir| 664 | assert_in_out_err([{"TZ"=>"UTC"}, *%W"-I#{@top_dir} -rlogger -C#{tmpdir} -"], "#{<<-"begin;"}\n#{<<-'end;'}") 665 | begin; 666 | begin 667 | module FakeTime 668 | attr_accessor :now 669 | end 670 | 671 | class << Time 672 | prepend FakeTime 673 | end 674 | 675 | log = "log" 676 | File.open(log, "w") {} 677 | Time.now = Time.utc(2015, 12, 14, 0, 1, 1) 678 | File.utime(Time.now, Time.now, log) 679 | 680 | dev = Logger::LogDevice.new("log", shift_age: 'monthly') 681 | 682 | Time.now = Time.utc(2015, 12, 31, 12, 34, 56) 683 | dev.write("#{Time.now} hello-1\n") 684 | File.utime(Time.now, Time.now, log) 685 | 686 | Time.now = Time.utc(2016, 1, 1, 0, 1, 1) 687 | dev.write("#{Time.now} hello-2\n") 688 | File.utime(Time.now, Time.now, log) 689 | ensure 690 | dev.close if dev 691 | end 692 | end; 693 | log = File.join(tmpdir, "log") 694 | cont = File.read(log) 695 | assert_match(/hello-2/, cont) 696 | assert_not_match(/hello-1/, cont) 697 | log = Dir.glob(log+".*") 698 | assert_equal(1, log.size) 699 | log, = *log 700 | cont = File.read(log) 701 | assert_match(/hello-1/, cont) 702 | assert_equal("2015-12-31", cont[/^[-\d]+/]) 703 | assert_equal("20151231", log[/\d+\z/]) 704 | end 705 | end if env_tz_works 706 | 707 | def test_shifting_dst_change 708 | Dir.mktmpdir do |tmpdir| 709 | assert_in_out_err([{"TZ"=>"Europe/London"}, *%W"--disable=gems -I#{@top_dir} -rlogger -C#{tmpdir} -"], "#{<<-"begin;"}\n#{<<-'end;'}") 710 | begin; 711 | begin 712 | module FakeTime 713 | attr_accessor :now 714 | end 715 | 716 | class << Time 717 | prepend FakeTime 718 | end 719 | 720 | log = "log" 721 | File.open(log, "w") {} 722 | 723 | Time.now = Time.mktime(2014, 3, 30, 0, 1, 1) 724 | File.utime(Time.now, Time.now, log) 725 | 726 | dev = Logger::LogDevice.new(log, shift_age: 'daily') 727 | dev.write("#{Time.now} hello-1\n") 728 | File.utime(*[Time.mktime(2014, 3, 30, 0, 2, 3)]*2, log) 729 | 730 | Time.now = Time.mktime(2014, 3, 31, 0, 1, 1) 731 | dev.write("#{Time.now} hello-2\n") 732 | File.utime(Time.now, Time.now, log) 733 | ensure 734 | dev.close 735 | end 736 | end; 737 | 738 | log = File.join(tmpdir, "log") 739 | cont = File.read(log) 740 | assert_match(/hello-2/, cont) 741 | assert_not_match(/hello-1/, cont) 742 | assert_file.exist?(log+".20140330") 743 | end 744 | end if env_tz_works 745 | 746 | def test_shifting_weekly_dst_change 747 | Dir.mktmpdir do |tmpdir| 748 | assert_separately([{"TZ"=>"Europe/London"}, *%W"-I#{@top_dir} -W0 -rlogger -C#{tmpdir} -"], "#{<<-"begin;"}\n#{<<-'end;'}") 749 | begin; 750 | begin 751 | module FakeTime 752 | attr_accessor :now 753 | end 754 | 755 | class << Time 756 | prepend FakeTime 757 | end 758 | 759 | log = "log" 760 | File.open(log, "w") {} 761 | Time.now = Time.mktime(2015, 10, 25, 0, 1, 1) 762 | File.utime(Time.now, Time.now, log) 763 | 764 | dev = Logger::LogDevice.new("log", shift_age: 'weekly') 765 | dev.write("#{Time.now} hello-1\n") 766 | File.utime(Time.now, Time.now, log) 767 | ensure 768 | dev.close if dev 769 | end 770 | end; 771 | log = File.join(tmpdir, "log") 772 | cont = File.read(log) 773 | assert_match(/hello-1/, cont) 774 | end 775 | end if env_tz_works 776 | 777 | def test_shifting_monthly_dst_change 778 | Dir.mktmpdir do |tmpdir| 779 | assert_separately([{"TZ"=>"Europe/London"}, *%W"-I#{@top_dir} -W0 -rlogger -C#{tmpdir} -"], "#{<<-"begin;"}\n#{<<-'end;'}") 780 | begin; 781 | begin 782 | module FakeTime 783 | attr_accessor :now 784 | end 785 | 786 | class << Time 787 | prepend FakeTime 788 | end 789 | 790 | log = "log" 791 | File.open(log, "w") {} 792 | Time.now = Time.utc(2016, 9, 1, 0, 1, 1) 793 | File.utime(Time.now, Time.now, log) 794 | 795 | dev = Logger::LogDevice.new("log", shift_age: 'monthly') 796 | 797 | Time.now = Time.utc(2016, 9, 8, 7, 6, 5) 798 | dev.write("#{Time.now} hello-1\n") 799 | File.utime(Time.now, Time.now, log) 800 | 801 | Time.now = Time.utc(2016, 10, 9, 8, 7, 6) 802 | dev.write("#{Time.now} hello-2\n") 803 | File.utime(Time.now, Time.now, log) 804 | 805 | Time.now = Time.utc(2016, 10, 9, 8, 7, 7) 806 | dev.write("#{Time.now} hello-3\n") 807 | File.utime(Time.now, Time.now, log) 808 | ensure 809 | dev.close if dev 810 | end 811 | end; 812 | log = File.join(tmpdir, "log") 813 | cont = File.read(log) 814 | assert_match(/hello-2/, cont) 815 | assert_not_match(/hello-1/, cont) 816 | log = Dir.glob(log+".*") 817 | assert_equal(1, log.size) 818 | log, = *log 819 | cont = File.read(log) 820 | assert_match(/hello-1/, cont) 821 | assert_equal("2016-09-08", cont[/^[-\d]+/]) 822 | assert_equal("20160930", log[/\d+\z/]) 823 | end 824 | end if env_tz_works 825 | 826 | def test_shifting_midnight_exist_file 827 | Dir.mktmpdir do |tmpdir| 828 | assert_in_out_err([*%W"--disable=gems -I#{@top_dir} -rlogger -C#{tmpdir} -"], "#{<<-"begin;"}\n#{<<-'end;'}") 829 | begin; 830 | begin 831 | module FakeTime 832 | attr_accessor :now 833 | end 834 | 835 | class << Time 836 | prepend FakeTime 837 | end 838 | 839 | log = "log" 840 | File.open(log, "w") {} 841 | File.utime(*[Time.mktime(2014, 1, 2, 0, 0, 0)]*2, log) 842 | 843 | Time.now = Time.mktime(2014, 1, 2, 23, 59, 59, 999000) 844 | dev = Logger::LogDevice.new(log, shift_age: 'daily') 845 | dev.write("#{Time.now} hello-1\n") 846 | dev.close 847 | File.utime(Time.now, Time.now, log) 848 | 849 | Time.now = Time.mktime(2014, 1, 3, 1, 1, 1) 850 | dev = Logger::LogDevice.new(log, shift_age: 'daily') 851 | dev.write("#{Time.now} hello-2\n") 852 | File.utime(Time.now, Time.now, log) 853 | ensure 854 | dev.close 855 | end 856 | end; 857 | 858 | bug = '[GH-539]' 859 | log = File.join(tmpdir, "log") 860 | cont = File.read(log) 861 | assert_match(/hello-2/, cont) 862 | assert_not_match(/hello-1/, cont) 863 | assert_file.for(bug).exist?(log+".20140102") 864 | assert_match(/hello-1/, File.read(log+".20140102"), bug) 865 | end 866 | end 867 | 868 | def test_shifting_weekly_exist_file 869 | Dir.mktmpdir do |tmpdir| 870 | assert_in_out_err([{"TZ"=>"UTC"}, *%W"-I#{@top_dir} -rlogger -C#{tmpdir} -"], "#{<<-"begin;"}\n#{<<-'end;'}") 871 | begin; 872 | begin 873 | module FakeTime 874 | attr_accessor :now 875 | end 876 | 877 | class << Time 878 | prepend FakeTime 879 | end 880 | 881 | log = "log" 882 | File.open(log, "w") {} 883 | Time.now = Time.utc(2015, 12, 14, 0, 1, 1) 884 | File.utime(Time.now, Time.now, log) 885 | 886 | dev = Logger::LogDevice.new("log", shift_age: 'weekly') 887 | 888 | Time.now = Time.utc(2015, 12, 19, 12, 34, 56) 889 | dev.write("#{Time.now} hello-1\n") 890 | dev.close 891 | File.utime(Time.now, Time.now, log) 892 | 893 | Time.now = Time.utc(2015, 12, 20, 0, 1, 1) 894 | dev = Logger::LogDevice.new("log", shift_age: 'weekly') 895 | dev.write("#{Time.now} hello-2\n") 896 | File.utime(Time.now, Time.now, log) 897 | ensure 898 | dev.close if dev 899 | end 900 | end; 901 | log = File.join(tmpdir, "log") 902 | cont = File.read(log) 903 | assert_match(/hello-2/, cont) 904 | assert_not_match(/hello-1/, cont) 905 | log = Dir.glob(log+".*") 906 | assert_equal(1, log.size) 907 | log, = *log 908 | cont = File.read(log) 909 | assert_match(/hello-1/, cont) 910 | assert_equal("2015-12-19", cont[/^[-\d]+/]) 911 | assert_equal("20151219", log[/\d+\z/]) 912 | end 913 | end if env_tz_works 914 | 915 | def test_shifting_monthly_exist_file 916 | Dir.mktmpdir do |tmpdir| 917 | assert_in_out_err([{"TZ"=>"UTC"}, *%W"-I#{@top_dir} -rlogger -C#{tmpdir} -"], "#{<<-"begin;"}\n#{<<-'end;'}") 918 | begin; 919 | begin 920 | module FakeTime 921 | attr_accessor :now 922 | end 923 | 924 | class << Time 925 | prepend FakeTime 926 | end 927 | 928 | log = "log" 929 | File.open(log, "w") {} 930 | Time.now = Time.utc(2015, 12, 14, 0, 1, 1) 931 | File.utime(Time.now, Time.now, log) 932 | 933 | dev = Logger::LogDevice.new("log", shift_age: 'monthly') 934 | 935 | Time.now = Time.utc(2015, 12, 31, 12, 34, 56) 936 | dev.write("#{Time.now} hello-1\n") 937 | dev.close 938 | File.utime(Time.now, Time.now, log) 939 | 940 | Time.now = Time.utc(2016, 1, 1, 0, 1, 1) 941 | dev = Logger::LogDevice.new("log", shift_age: 'monthly') 942 | dev.write("#{Time.now} hello-2\n") 943 | File.utime(Time.now, Time.now, log) 944 | ensure 945 | dev.close if dev 946 | end 947 | end; 948 | log = File.join(tmpdir, "log") 949 | cont = File.read(log) 950 | assert_match(/hello-2/, cont) 951 | assert_not_match(/hello-1/, cont) 952 | log = Dir.glob(log+".*") 953 | assert_equal(1, log.size) 954 | log, = *log 955 | cont = File.read(log) 956 | assert_match(/hello-1/, cont) 957 | assert_equal("2015-12-31", cont[/^[-\d]+/]) 958 | assert_equal("20151231", log[/\d+\z/]) 959 | end 960 | end if env_tz_works 961 | 962 | private 963 | 964 | def run_children(n, args, src) 965 | r, w = IO.pipe 966 | [w, *(1..n).map do 967 | f = IO.popen([EnvUtil.rubybin, *%w[--disable=gems -], *args], "w", err: w) 968 | src = "$LOAD_PATH.unshift('#{@top_dir}'); require 'logger';#{src}" 969 | f.puts(src) 970 | f 971 | end].each(&:close) 972 | stderr = r.read 973 | r.close 974 | stderr 975 | end 976 | end 977 | --------------------------------------------------------------------------------