summaryrefslogtreecommitdiff
path: root/lib/mixlib/log.rb
blob: 5ec1ba06922dbe3f95c08077de45f5e4e6ecb149 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Christopher Brown (<cb@chef.io>)
# Copyright:: Copyright (c) 2008-2016 Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require "logger"
require "mixlib/log/version"
require "mixlib/log/formatter"

module Mixlib
  module Log

    @logger, @loggers = nil

    LEVELS = { :debug => Logger::DEBUG, :info => Logger::INFO, :warn => Logger::WARN, :error => Logger::ERROR, :fatal => Logger::FATAL }.freeze
    LEVEL_NAMES = LEVELS.invert.freeze

    def reset!
      close!
      @logger, @loggers = nil, nil
    end

    # An Array of log devices that will be logged to. Defaults to just the default
    # @logger log device, but you can push to this array to add more devices.
    def loggers
      @loggers ||= [logger]
    end

    ##
    # init always returns a configured logger
    # and creates a new one if it doesn't yet exist
    ##
    def logger
      @logger || init
    end

    # Sets the log device to +new_log_device+. Any additional loggers
    # that had been added to the +loggers+ array will be cleared.
    def logger=(new_log_device)
      reset!
      @logger = new_log_device
    end

    def use_log_devices(other)
      if other.respond_to?(:loggers) && other.respond_to?(:logger)
        @loggers = other.loggers
        @logger = other.logger
      elsif other.kind_of?(Array)
        @loggers = other
        @logger = other.first
      else
        msg = "#use_log_devices takes a Mixlib::Log object or array of log devices. " <<
          "You gave: #{other.inspect}"
        raise ArgumentError, msg
      end
    end

    # Use Mixlib::Log.init when you want to set up the logger manually.  Arguments to this method
    # get passed directly to Logger.new, so check out the documentation for the standard Logger class
    # to understand what to do here.
    #
    # If this method is called with no arguments, it will log to STDOUT at the :warn level.
    #
    # It also configures the Logger instance it creates to use the custom Mixlib::Log::Formatter class.
    def init(*opts)
      reset!
      @logger = logger_for(*opts)
      @logger.formatter = Mixlib::Log::Formatter.new() if @logger.respond_to?(:formatter=)
      @logger.level = Logger::WARN
      @logger
    end

    # Sets the level for the Logger object by symbol.  Valid arguments are:
    #
    #  :debug
    #  :info
    #  :warn
    #  :error
    #  :fatal
    #
    # Throws an ArgumentError if you feed it a bogus log level.
    def level=(new_level)
      level_int = LEVEL_NAMES.key?(new_level) ? new_level : LEVELS[new_level]
      raise ArgumentError, "Log level must be one of :debug, :info, :warn, :error, or :fatal" if level_int.nil?
      loggers.each { |l| l.level = level_int }
    end

    def level(new_level = nil)
      if new_level.nil?
        LEVEL_NAMES[logger.level]
      else
        self.level = (new_level)
      end
    end

    # Define the standard logger methods on this class programmatically.
    # No need to incur method_missing overhead on every log call.
    [:debug, :info, :warn, :error, :fatal].each do |method_name|
      class_eval(<<-METHOD_DEFN, __FILE__, __LINE__)
        def #{method_name}(msg=nil, &block)
          loggers.each {|l| l.#{method_name}(msg, &block) }
        end
      METHOD_DEFN
    end

    # Define the methods to interrogate the logger for the current log level.
    # Note that we *only* query the default logger (@logger) and not any other
    # loggers that may have been added, even though it is possible to configure
    # two (or more) loggers at different log levels.
    [:debug?, :info?, :warn?, :error?, :fatal?].each do |method_name|
      class_eval(<<-METHOD_DEFN, __FILE__, __LINE__)
        def #{method_name}
          logger.#{method_name}
        end
      METHOD_DEFN
    end

    def <<(msg)
      loggers.each { |l| l << msg }
    end

    def add(severity, message = nil, progname = nil, &block)
      loggers.each { |l| l.add(severity, message, progname, &block) }
    end

    alias :log :add

    # Passes any other method calls on directly to the underlying Logger object created with init. If
    # this method gets hit before a call to Mixlib::Logger.init has been made, it will call
    # Mixlib::Logger.init() with no arguments.
    def method_missing(method_symbol, *args, &block)
      loggers.each { |l| l.send(method_symbol, *args, &block) }
    end

    private

    def logger_for(*opts)
      if opts.empty?
        Logger.new(STDOUT)
      elsif LEVELS.keys.inject(true) { |quacks, level| quacks && opts.first.respond_to?(level) }
        opts.first
      else
        Logger.new(*opts)
      end
    end

    def all_loggers
      [@logger, *@loggers].uniq
    end

    # select all loggers with File log devices
    def loggers_to_close
      loggers_to_close = []
      all_loggers.each do |logger|
        # unfortunately Logger does not provide access to the logdev
        # via public API. In order to reduce amount of impact and
        # handle only File type log devices I had to use this method
        # to get access to it.
        next unless logdev = logger.instance_variable_get(:"@logdev")
        loggers_to_close << logger if logdev.filename
      end
      loggers_to_close
    end

    def close!
      # try to close all file loggers
      loggers_to_close.each do |l|
        l.close rescue nil
      end
    end

  end
end