aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/buffered_logger.rb
blob: 136e245859861f498bd2b2ffa74479fb974c0214 (plain) (blame)
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
require 'thread'
require 'active_support/core_ext/class/attribute_accessors'

module ActiveSupport
  # Inspired by the buffered logger idea by Ezra
  class BufferedLogger
    module Severity
      DEBUG   = 0
      INFO    = 1
      WARN    = 2
      ERROR   = 3
      FATAL   = 4
      UNKNOWN = 5
    end
    include Severity

    MAX_BUFFER_SIZE = 1000

    ##
    # :singleton-method:
    # Set to false to disable the silencer
    cattr_accessor :silencer
    self.silencer = true

    # Silences the logger for the duration of the block.
    def silence(temporary_level = ERROR)
      if silencer
        old_logger_level = @tmp_levels[Thread.current]
        begin
          @tmp_levels[Thread.current] = temporary_level
          yield self
        ensure
          if old_logger_level
            @tmp_levels[Thread.current] = old_logger_level
          else
            @tmp_levels.delete(Thread.current)
          end
        end
      else
        yield self
      end
    end

    attr_writer :level
    attr_reader :auto_flushing

    def initialize(log, level = DEBUG)
      @level         = level
      @tmp_levels    = {}
      @buffer        = Hash.new { |h,k| h[k] = [] }
      @auto_flushing = 1
      @guard = Mutex.new

      if log.respond_to?(:write)
        @log = log
      elsif File.exist?(log)
        @log = open_log(log, (File::WRONLY | File::APPEND))
      else
        FileUtils.mkdir_p(File.dirname(log))
        @log = open_log(log, (File::WRONLY | File::APPEND | File::CREAT))
      end
    end

    def open_log(log, mode)
      open(log, mode).tap do |open_log|
        open_log.set_encoding(Encoding::BINARY) if open_log.respond_to?(:set_encoding)
        open_log.sync = true
      end
    end

    def level
      @tmp_levels[Thread.current] || @level
    end

    def add(severity, message = nil, progname = nil, &block)
      return if level > severity
      message = (message || (block && block.call) || progname).to_s
      # If a newline is necessary then create a new message ending with a newline.
      # Ensures that the original message is not mutated.
      message = "#{message}\n" unless message[-1] == ?\n
      buffer << message
      auto_flush
      message
    end

    # Dynamically add methods such as:
    # def info
    # def warn
    # def debug
    Severity.constants.each do |severity|
      class_eval <<-EOT, __FILE__, __LINE__ + 1
        def #{severity.downcase}(message = nil, progname = nil, &block) # def debug(message = nil, progname = nil, &block)
          add(#{severity}, message, progname, &block)                   #   add(DEBUG, message, progname, &block)
        end                                                             # end

        def #{severity.downcase}?                                       # def debug?
          #{severity} >= level                                         #   DEBUG >= @level
        end                                                             # end
      EOT
    end

    # Set the auto-flush period. Set to true to flush after every log message,
    # to an integer to flush every N messages, or to false, nil, or zero to
    # never auto-flush. If you turn auto-flushing off, be sure to regularly
    # flush the log yourself -- it will eat up memory until you do.
    def auto_flushing=(period)
      @auto_flushing =
        case period
        when true;                1
        when false, nil, 0;       MAX_BUFFER_SIZE
        when Integer;             period
        else raise ArgumentError, "Unrecognized auto_flushing period: #{period.inspect}"
        end
    end

    def flush
      @guard.synchronize do
        write_buffer(buffer)

        # Important to do this even if buffer was empty or else @buffer will
        # accumulate empty arrays for each request where nothing was logged.
        clear_buffer

        # Clear buffers associated with dead threads or else spawned threads
        # that don't call flush will result in a memory leak.
        flush_dead_buffers
      end
    end

    def close
      flush
      @log.close if @log.respond_to?(:close)
      @log = nil
    end

    protected
      def auto_flush
        flush if buffer.size >= @auto_flushing
      end

      def buffer
        @buffer[Thread.current]
      end

      def clear_buffer
        @buffer.delete(Thread.current)
      end

      # Find buffers created by threads that are no longer alive and flush them to the log
      # in order to prevent memory leaks from spawned threads.
      def flush_dead_buffers #:nodoc:
        @buffer.keys.reject{|thread| thread.alive?}.each do |thread|
          buffer = @buffer[thread]
          write_buffer(buffer)
          @buffer.delete(thread)
        end
      end

      def write_buffer(buffer)
        buffer.each do |content|
          @log.write(content)
        end
      end
  end
end