log_mixin 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +92 -0
- data/lib/log_mixin.rb +263 -0
- data/log_mixin.gemspec +36 -0
- metadata +92 -0
data/README.md
ADDED
@@ -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
|
+
|
data/lib/log_mixin.rb
ADDED
@@ -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
|
data/log_mixin.gemspec
ADDED
@@ -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:
|