log_mixin 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.md +92 -0
  2. data/lib/log_mixin.rb +263 -0
  3. data/log_mixin.gemspec +36 -0
  4. metadata +92 -0
@@ -0,0 +1,92 @@
1
+ LogMixin
2
+ ===
3
+
4
+ The LogMixin module provides (you guessed it) a mixin to make logging more
5
+ convenient. It is intended to work both with and without Rails, to silence
6
+ logging in tests by default but provide access to it when explicitly
7
+ requested, to log to stderr (or Rails.logger) by default but accept other
8
+ logging targets, and other conveniences.
9
+
10
+ Released under the three-clause BSD open source license.
11
+ http://opensource.org/licenses/BSD-3-Clause
12
+
13
+ Usage
14
+ ===
15
+ ```ruby
16
+ class Foo
17
+ include LogMixin
18
+
19
+ def do_something_harmless
20
+ self.debug("Arcane internal debugging parameters: foo=#{foo} bar=#{bar}")
21
+ self.info("Doing something harmless...")
22
+ # ...
23
+ end
24
+
25
+ def do_something_risky
26
+ if self.on_fire?
27
+ self.err("Ignition initiated")
28
+ self.fatal("HELP! I'm on fire!") if on_fire
29
+ else
30
+ self.warn("Temperature is rising, but not on fire yet")
31
+ end
32
+ # ...
33
+ end
34
+ end
35
+ ```
36
+
37
+ Logging and Tests
38
+ ===
39
+ By default, log messages are silenced during tests. This is usually what
40
+ you want.
41
+
42
+ Sometimes you may want to test logging behavior. It's usually not advisable
43
+ to test the *content* of log messages. If you know what you're signing up
44
+ for, you might test that an INFO message was logged rather than a WARNING,
45
+ however. Be careful -- you're creating an implicit contract. That having
46
+ been said, here is how you can test log messages in RSpec:
47
+ ```ruby
48
+ it 'should log correctly' do
49
+ obj = MyClass.new
50
+ obj.__handle.msgs.should have(0).messages
51
+ obj.do_something_that_logs_twice
52
+ obj.__handle.msgs.should have(2).messages
53
+ obj.do_something_that_logs_an_error
54
+ obj.__handle.msgs.select {|msg| msg =~ /ERROR/}.should have(1).message
55
+ end
56
+ ```
57
+
58
+ What if you're running your tests and you have a bug, but you can't figure
59
+ out what's going on, so you actually _want_ to see the log messages? Well,
60
+ you can. Here is how you activate logging during tests, which you should
61
+ presumably only do temporarily:
62
+ ```ruby
63
+ ['TESTING', 'RAILS'].each do |c|
64
+ LogMixin.send(:remove_const, c)
65
+ LogMixin.const_set(c, false)
66
+ end
67
+ ```
68
+
69
+ LogMixin supports both inclusion (i.e., instances of the class get the
70
+ logging methods) and extension (i.e., the class or module itself get the
71
+ logging methods).
72
+ ```ruby
73
+ class ChattyInstance
74
+ include LogMixin
75
+ ...
76
+ end
77
+
78
+ module ChattyModule
79
+ extend LogMixin
80
+ ...
81
+ end
82
+
83
+ c = ChattyInstance.new
84
+ c.info("I'm an instance!")
85
+ ChattyModule.info("I'm a module!")
86
+ LogMixin.info("You can even log with LogMixin itself...")
87
+ LogMixin.warn("...but 'mixin' is a bit of a misnomer in that case.")
88
+ ```
89
+
90
+ Want to change the log message format? You have lots of flexibility. See
91
+ the documentation for ```#configure_logs``` and ```VBLM_DEFAULT_FORMAT```.
92
+
@@ -0,0 +1,263 @@
1
+ # Mix-in class to support logging under Heroku, local Rails, command line,
2
+ # test environment, etc. Especially useful for utilities which might not
3
+ # be run under Rails, so Rails.logger isn't an option.
4
+ #
5
+ # Usage:
6
+ # class MyClass
7
+ # include LogMixin
8
+ # # if you write an initializer, invoke configure_logs there -- see below
9
+ # ...
10
+ # end
11
+ #
12
+ # obj = MyClass.new
13
+ # obj.info("Something happened!") # default severity is INFO
14
+ # obj.fatal("EMERGENCY!")
15
+ #
16
+ # Testing the contents of log messages is suspicious, but you may want to
17
+ # test that logging occurred, and in any case, we won't stop you from testing
18
+ # what you want. Here's how you access the logging from a test:
19
+ #
20
+ # it 'should log correctly' do
21
+ # obj = MyClass.new
22
+ # obj.__handle.msgs.should have(0).messages
23
+ # obj.do_something_that_logs_twice
24
+ # obj.__handle.msgs.should have(2).messages
25
+ # obj.do_something_that_logs_an_error
26
+ # obj.__handle.msgs.select {|msg| msg =~ /ERROR/}.should have(1).message
27
+ # end
28
+ #
29
+ # Want to hack your tests so that you temporarily output log messages?
30
+ # ['TESTING', 'RAILS'].each do |c|
31
+ # LogMixin.send(:remove_const, c)
32
+ # LogMixin.const_set(c, false)
33
+ # end
34
+ #
35
+
36
+ require 'time' # for time-stamping log messages
37
+
38
+ module LogMixin
39
+
40
+ class Error < StandardError; end
41
+ class InitError < Error; end
42
+
43
+ # If your class has its own initializer, you probably want to invoke
44
+ # configure_logs in that initializer. If you don't write an initializer
45
+ # and use the default, this should suffice.
46
+ def initialize(*args)
47
+ super(*args)
48
+ configure_logs # with no other info, configure with default params
49
+ end
50
+
51
+ ############################## FOR TEST ONLY ##############################
52
+
53
+ # Special-case the condition of running under a test environment
54
+ # We probably don't want to spew logging output when running unit tests
55
+ TESTING = Object.constants.include?(:RSpec)
56
+
57
+ # FakeFileHandle is a place to send test logging output
58
+ class FakeFileHandle
59
+ attr_reader :msgs
60
+ def initialize; @msgs = []; end
61
+ def print(msg); @msgs << msg; end
62
+ end
63
+
64
+ ############################## END TEST ONLY ##############################
65
+
66
+ # If we're running under Rails, then we'll probably want to use the Rails
67
+ # logging facilities.
68
+ RAILS = Object.constants.include? :Rails
69
+
70
+ # Log levels: 0 = CRITICAL, 1 = ERROR, 2 = WARNING, 3 = INFO, 4 = DEBUG
71
+ # Any other integer is also valid, but doesn't map to a named level.
72
+ # An *object's* log level is its threshold for caring about log messages.
73
+ # A *message's* log level is its importance.
74
+ #
75
+
76
+ VBLM_LOG_LEVEL_NAMES = {
77
+ :critical => 0,
78
+ :error => 1,
79
+ :warning => 2,
80
+ :info => 3,
81
+ :debug => 4,
82
+ }
83
+ VBLM_DEFAULT_LOG_LEVEL = :info
84
+
85
+ # Default format looks like this:
86
+ # "[2012-04-18 12:00:00] MyObject: ERROR: Something went wrong"
87
+ # Note timestamp, caller, severity, message
88
+ #
89
+ # When creating custom formats, :caller can be either a fixed string (such as
90
+ # the program name or '') or a function on the calling object (as below).
91
+ # :severity can be a fixed string (usually '') or a function on the
92
+ # integer log_level.
93
+ # :timestamp must be a string, and may include any format chars understood by
94
+ # Time.strftime
95
+ VBLM_DEFAULT_FORMAT = {
96
+ :timestamp => '[%Y-%m-%d %H:%M:%S] ',
97
+ :caller => lambda do |ob|
98
+ %{Module Class}.include?(ob.class.name) ?
99
+ "#{ob.name}: " : "#{ob.class.name}: "
100
+ end,
101
+ :severity => lambda { |level|
102
+ level = self.log_level_int(level)
103
+ ((level >= 0 and level <= 4) ?
104
+ %W{CRITICAL ERROR WARNING INFO DEBUG}[level] :
105
+ "Log level #{level.to_s}") + ": " },
106
+ }
107
+
108
+
109
+ # We want to handle "log_level = 3" and "log_level = :info" with equal grace.
110
+ # This function canonicalizes its input to an integer (e.g. :info becomes 3)
111
+ def self.log_level_int(symbol_or_int)
112
+ if symbol_or_int.class == Symbol && VBLM_LOG_LEVEL_NAMES.key?(symbol_or_int)
113
+ return VBLM_LOG_LEVEL_NAMES[symbol_or_int]
114
+ end
115
+ if symbol_or_int.class == Fixnum
116
+ return symbol_or_int
117
+ end
118
+ raise ArgumentError, "Invalid log level: #{symbol_or_int.to_s}"
119
+ end
120
+
121
+ # Any object using the LogMixin module must call configure_logs() before
122
+ # starting to log.
123
+ # * :log_handles is a list of objects which respond to "print"
124
+ # * :log_level is the maximum (lowest-priority) level that this object cares
125
+ # about logging -- log messages of higher log_level (lower priority)
126
+ # will be ignored
127
+ # * :format is a hash with any or all of [:timestamp, :caller, :severity]
128
+ # * keys which are nil or not overridden will retain default values
129
+ # * for details of hash values, see VBLM_DEFAULT_FORMAT above
130
+ #
131
+ def configure_logs(options={})
132
+ default_options = {
133
+ :log_handles => [$stdout],
134
+ :log_level => VBLM_DEFAULT_LOG_LEVEL,
135
+ :format => VBLM_DEFAULT_FORMAT,
136
+ :test_logging_internals => false,
137
+ }
138
+ opts = default_options
139
+ opts.update(options)
140
+ # Are we testing? If so, don't output logs, just keep them in memory.
141
+ # (Unless we're testing the internals of the logging module.)
142
+ if TESTING and not opts[:test_logging_internals] then
143
+ @__handle = FakeFileHandle.new
144
+ @vblm_log_handles = [@__handle]
145
+ else
146
+ @vblm_log_handles = opts[:log_handles] || []
147
+ end
148
+ @vblm_log_level = opts[:log_level]
149
+ @vblm_format = opts[:format] || VBLM_DEFAULT_FORMAT
150
+ # Caller can request e.g. custom timestamp, keeping other default formatting
151
+ [:timestamp, :caller, :severity].each do |key|
152
+ @vblm_format[key] ||= VBLM_DEFAULT_FORMAT[key]
153
+ end
154
+ @__log_mixin_initialized = true
155
+ end
156
+
157
+ # For changing log level of a running process, e.g. via HTTP
158
+ def setLogLevel(log_level)
159
+ @vblm_log_level = log_level
160
+ end
161
+
162
+ # Handle parameters (formatting options, in our case) which may either be
163
+ # fixed strings or functions returning strings. If the input is a function,
164
+ # call it with the given *args and return its result.
165
+ #
166
+ def stringOrFunctionOutput(s_or_f, *args)
167
+ s_or_f.respond_to?('call') ? s_or_f.call(*args) : s_or_f.to_s
168
+ end
169
+
170
+ # Return the message formatted according to this LogMixin object's params
171
+ # (Usually this means prepending a timestamp, caller, and severity, and
172
+ # adding a newline.)
173
+ #
174
+ def formattedMessage(msg, options={})
175
+ default_options = { :log_level => VBLM_DEFAULT_LOG_LEVEL }
176
+ opts = default_options
177
+ opts.update(options)
178
+ timestamp = Time.now.strftime(@vblm_format[:timestamp])
179
+ # :caller and :severity may be fixed strings, or functions returning strings
180
+ caller = stringOrFunctionOutput(@vblm_format[:caller], self)
181
+ severity = stringOrFunctionOutput(@vblm_format[:severity], opts[:log_level])
182
+ "#{timestamp}#{caller}#{severity}#{msg}\n"
183
+ end
184
+
185
+ # Use Rails.logger to do the logging
186
+ # This assumes that the caller has detected a Rails environment.
187
+ #
188
+ def __railsLog(msg, level)
189
+ # First, squash log levels worse than CRITICAL to CRITICAL, and levels
190
+ # more benign than DEBUG to DEBUG.
191
+ rails_level = level
192
+ if rails_level < LogMixin::log_level_int(:critical) then
193
+ rails_level = LogMixin::log_level_int(:critical)
194
+ elsif rails_level > LogMixin::log_level_int(:debug) then
195
+ rails_level = LogMixin::log_level_int(:debug)
196
+ end
197
+ case rails_level
198
+ when LogMixin::log_level_int(:debug) then Rails.logger.debug(msg)
199
+ when LogMixin::log_level_int(:info) then Rails.logger.info(msg)
200
+ when LogMixin::log_level_int(:warning) then Rails.logger.warn(msg)
201
+ when LogMixin::log_level_int(:error) then Rails.logger.error(msg)
202
+ when LogMixin::log_level_int(:critical) then Rails.logger.fatal(msg)
203
+ end
204
+ end
205
+ # Print the message to "all" logging handles, if the message level is of
206
+ # sufficient priority that this object cares about it (i.e. if the numeric
207
+ # message log level is <= the object log level). Silently discard the
208
+ # message if its priority is insufficient.
209
+ #
210
+ # "All" filehandles include everything in @vblm_log_handles, except that
211
+ # if we are running under Rails, the default handle $stdout is ignored
212
+ # and replaced with the Rails logger.
213
+ #
214
+ def log(msg, options={})
215
+ if not @__log_mixin_initialized
216
+ raise InitError, "#{self.class} object uses LogMixin " +
217
+ "but never called configure_logs"
218
+ end
219
+ default_options = {:level => VBLM_DEFAULT_LOG_LEVEL}
220
+ opts = default_options
221
+ opts.update(options)
222
+
223
+ level = LogMixin::log_level_int(opts[:level])
224
+ return if level > LogMixin::log_level_int(@vblm_log_level)
225
+
226
+ # Write the log message, in the appropriate style
227
+ formatted_msg = self.formattedMessage(msg, :log_level => level)
228
+ if RAILS then
229
+ # log Rails-style, with Rails.logger.{debug,info,warn,error,fatal}
230
+ __railsLog(msg, level)
231
+ else
232
+ # format message, and print to $stdout (unless $stdout isn't requested)
233
+ $stdout.print(formatted_msg) if @vblm_log_handles.include?($stdout)
234
+ end
235
+
236
+ # Print to the provided file handles (except stdout, which we've covered)
237
+ @vblm_log_handles.reject {|h| h == $stdout}.each do |handle|
238
+ # Support lazy-evaluation of log handles, so that loggable objects can
239
+ # be instantiated without creating log files until they actually
240
+ # log something.
241
+ if handle.respond_to?(:call) then
242
+ @vblm_log_handles[@vblm_log_handles.index(handle)] = handle.call
243
+ handle = handle.call
244
+ end
245
+
246
+ handle.print(formatted_msg)
247
+ end
248
+ end
249
+
250
+ def debug(msg, options={}); log(msg, options.merge(level: :debug )); end
251
+ def info(msg, options={}); log(msg, options.merge(level: :info )); end
252
+ def warn(msg, options={}); log(msg, options.merge(level: :warning )); end
253
+ def err(msg, options={}); log(msg, options.merge(level: :error )); end
254
+ def fatal(msg, options={}); log(msg, options.merge(level: :critical)); end
255
+
256
+ extend self # can be invoked as module methods or object methods
257
+ configure_logs
258
+
259
+ # The configure_logs call above doesn't apply when some other module extends
260
+ # LogMixin, so the following call addresses that.
261
+ def self.extended(base); base.configure_logs; end
262
+
263
+ end # module LogMixin
@@ -0,0 +1,36 @@
1
+
2
+ Gem::Specification.new do |s|
3
+ s.name = %q{log_mixin}
4
+ s.version = '1.1.2'
5
+ s.platform = Gem::Platform::RUBY
6
+ s.required_ruby_version = '>= 1.9.3'
7
+
8
+ if s.respond_to? :required_rubygems_version=
9
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2")
10
+ end
11
+ s.authors = ['Jon Snitow']
12
+ s.email = ['opensource@verticalbrands.com']
13
+ s.date = '2012-10-19'
14
+ s.summary = 'Mixin module for easy logging under various circumstances'
15
+ s.description = <<-EOT
16
+ The LogMixin module provides (you guessed it) a mixin to make logging more
17
+ convenient. It is intended to work both with and without Rails, to silence
18
+ logging in tests by default but provide access to it when explicitly
19
+ requested, to log to stderr (or Rails.logger) by default but accept other
20
+ logging targets, and other conveniences.
21
+ EOT
22
+ s.files = Dir[
23
+ '{lib}/**/*.rb',
24
+ 'LICENSE',
25
+ '*.md',
26
+ 'log_mixin.gemspec',
27
+ ]
28
+ s.require_paths = ['lib']
29
+
30
+ s.rubyforge_project = 'log_mixin'
31
+ s.rubygems_version = '>= 1.8.6'
32
+ s.homepage = 'http://github.com/apartmentlist'
33
+
34
+ s.add_development_dependency('rspec')
35
+ s.add_development_dependency('rr')
36
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: log_mixin
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jon Snitow
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rr
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: ! 'The LogMixin module provides (you guessed it) a mixin to make logging
47
+ more
48
+
49
+ convenient. It is intended to work both with and without Rails, to silence
50
+
51
+ logging in tests by default but provide access to it when explicitly
52
+
53
+ requested, to log to stderr (or Rails.logger) by default but accept other
54
+
55
+ logging targets, and other conveniences.
56
+
57
+ '
58
+ email:
59
+ - opensource@verticalbrands.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - lib/log_mixin.rb
65
+ - README.md
66
+ - log_mixin.gemspec
67
+ homepage: http://github.com/apartmentlist
68
+ licenses: []
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: 1.9.3
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ! '>='
83
+ - !ruby/object:Gem::Version
84
+ version: '1.2'
85
+ requirements: []
86
+ rubyforge_project: log_mixin
87
+ rubygems_version: 1.8.19
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: Mixin module for easy logging under various circumstances
91
+ test_files: []
92
+ has_rdoc: