log_mixin 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: