chalk-log 0.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.
@@ -0,0 +1,63 @@
1
+ # Thin wrapper over Logging::Logger. This is the per-class object
2
+ # instantiated by the `log` method.
3
+ class Chalk::Log::Logger
4
+ attr_reader :backend
5
+
6
+ # Initialization of the logger backend. It does the actual creation
7
+ # of the various logger methods. Will be called automatically upon
8
+ # your first `log` method call.
9
+ def self.init
10
+ Chalk::Log::LEVELS.each do |level|
11
+ define_method(level) do |*data, &blk|
12
+ return if logging_disabled?
13
+ @backend.send(level, data, &blk)
14
+ end
15
+ end
16
+ end
17
+
18
+ # The level this logger is set to.
19
+ def level
20
+ @backend.level
21
+ end
22
+
23
+ # Set the maximum log level.
24
+ #
25
+ # @param level [Fixnum|String|Symbol] A valid Logging::Logger level, e.g. :debug, 0, 'DEBUG', etc.
26
+ def level=(level)
27
+ @backend.level = level
28
+ end
29
+
30
+ # Create a new logger, and auto-initialize everything.
31
+ def initialize(name)
32
+ # It's generally a bad pattern to auto-init, but we want
33
+ # Chalk::Log to be usable anytime during the boot process, which
34
+ # requires being a little bit less explicit than we usually like.
35
+ Chalk::Log.init
36
+ @backend = ::Logging::Logger.new(name)
37
+ if level = configatron.chalk.log.default_level
38
+ @backend.level = level
39
+ end
40
+ end
41
+
42
+ # Check whether logging has been globally turned off, either through
43
+ # configatron or LSpace.
44
+ def logging_disabled?
45
+ configatron.chalk.log.disabled || LSpace[:'chalk.log.disabled']
46
+ end
47
+
48
+ def with_contextual_info(contextual_info={}, &blk)
49
+ unless blk
50
+ raise ArgumentError.new("Must pass a block to #{__method__}")
51
+ end
52
+ unless contextual_info.is_a?(Hash)
53
+ raise TypeError.new(
54
+ "contextual_info must be a Hash, but got #{contextual_info.class}"
55
+ )
56
+ end
57
+ existing_context = LSpace[:'chalk.log.contextual_info'] || {}
58
+ LSpace.with(
59
+ :'chalk.log.contextual_info' => contextual_info.merge(existing_context),
60
+ &blk
61
+ )
62
+ end
63
+ end
@@ -0,0 +1,84 @@
1
+ module Chalk::Log::Utils
2
+ # Nicely formats a backtrace:
3
+ #
4
+ # ```ruby
5
+ # format_backtrace(['line1', 'line2'])
6
+ # #=> line1
7
+ # line2
8
+ # ```
9
+ #
10
+ # (Used internally when `Chalk::Log` is formatting exceptions.)
11
+ #
12
+ # TODO: add autotruncating of backtraces.
13
+ def self.format_backtrace(backtrace)
14
+ if configatron.chalk.log.compress_backtraces
15
+ backtrace = compress_backtrace(backtrace)
16
+ backtrace << '(Disable backtrace compression by setting configatron.chalk.log.compress_backtraces = false.)'
17
+ end
18
+
19
+ " " + backtrace.join("\n ")
20
+ end
21
+
22
+ # Explodes a nested hash to just have top-level keys. This is
23
+ # generally useful if you have something that knows how to parse
24
+ # kv-pairs.
25
+ #
26
+ # ```ruby
27
+ # explode_nested_hash(foo: {bar: 'baz', bat: 'zom'})
28
+ # #=> {'foo_bar' => 'baz', 'foo_bat' => 'zom'}
29
+ # ```
30
+ def self.explode_nested_hash(hash, prefix=nil)
31
+ exploded = {}
32
+
33
+ hash.each do |key, value|
34
+ new_prefix = prefix ? "#{prefix}_#{key}" : key.to_s
35
+
36
+ if value.is_a?(Hash)
37
+ exploded.merge!(self.explode_nested_hash(value, new_prefix))
38
+ else
39
+ exploded[new_prefix] = value
40
+ end
41
+ end
42
+
43
+ exploded
44
+ end
45
+
46
+ # Compresses a backtrace, omitting gem lines (unless they appear
47
+ # before any application lines).
48
+ def self.compress_backtrace(backtrace)
49
+ compressed = []
50
+ gemdir = Gem.dir
51
+
52
+ hit_application = false
53
+ # This isn't currently read by anything, but we could easily use
54
+ # it to limit the number of leading gem lines.
55
+ leading_lines = 0
56
+ gemlines = 0
57
+ backtrace.each do |line|
58
+ if line.start_with?(gemdir)
59
+ # If we're in a gem, always increment the counter. Record the
60
+ # first three lines if we haven't seen any application lines
61
+ # yet.
62
+ if !hit_application
63
+ compressed << line
64
+ leading_lines += 1
65
+ else
66
+ gemlines += 1
67
+ end
68
+ elsif gemlines > 0
69
+ # If we were in a gem and now are not, record the number of
70
+ # lines skipped.
71
+ compressed << "<#{gemlines} #{gemlines == 1 ? 'line' : 'lines'} omitted>"
72
+ compressed << line
73
+ hit_application = true
74
+ gemlines = 0
75
+ else
76
+ # If we're in the application, always record the line.
77
+ compressed << line
78
+ hit_application = true
79
+ end
80
+ end
81
+
82
+ compressed
83
+ end
84
+ end
@@ -0,0 +1,5 @@
1
+ module Chalk
2
+ module Log
3
+ VERSION = '0.1.2'
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ :tddium:
2
+ :ruby_version: ruby-1.9.3-p194
3
+ :bundler_version: 1.3.5
4
+ :test_pattern:
5
+ - "test/wholesome/[^_]*.rb"
6
+ - "test/unit/[^_]*.rb"
7
+ - "test/integration/[^_]*.rb"
8
+ - "test/functional/[^_]*.rb"
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'minitest/autorun'
5
+ require 'minitest/spec'
6
+ require 'mocha/setup'
7
+
8
+ module Critic
9
+ require_relative '_lib/fake'
10
+
11
+ class Test < ::MiniTest::Spec
12
+ def setup
13
+ # Put any stubs here that you want to apply globally
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ module Critic::Fake
2
+ Dir[File.expand_path('../fake/*.rb', __FILE__)].each do |file|
3
+ require_relative(file)
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ class Critic::Fake::Event
2
+ attr_reader :data, :time, :level
3
+
4
+ def initialize(opts)
5
+ @data = opts.fetch(:data)
6
+ @time = opts.fetch(:time, Time.new(1979,4,9))
7
+ @level = opts.fetch(:level, 1)
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ require File.expand_path('../../_lib', __FILE__)
2
+
3
+ module Critic::Functional
4
+ module Stubs
5
+ end
6
+
7
+ class Test < Critic::Test
8
+ include Stubs
9
+ end
10
+ end
@@ -0,0 +1,188 @@
1
+ require File.expand_path('../_lib', __FILE__)
2
+
3
+ require 'chalk-log'
4
+
5
+ module Critic::Functional
6
+ class LogTest < Test
7
+ def enable_timestamp
8
+ configatron.chalk.log.stubs(:timestamp).returns(true)
9
+ end
10
+
11
+ def disable_timestamp
12
+ configatron.chalk.log.stubs(:timestamp).returns(false)
13
+ end
14
+
15
+ def disable_pid
16
+ configatron.chalk.log.stubs(:pid).returns(false)
17
+ end
18
+
19
+ def disable_tagging
20
+ configatron.chalk.log.stubs(:tagging).returns(false)
21
+ end
22
+
23
+ before do
24
+ Chalk::Log.init
25
+ Process.stubs(:pid).returns(9973)
26
+ disable_timestamp
27
+ end
28
+
29
+ class MyClass
30
+ include Chalk::Log
31
+ end
32
+
33
+ describe 'when called without arguments' do
34
+ it 'does not loop infinitely' do
35
+ MyClass.log.info
36
+ end
37
+ end
38
+
39
+ describe 'when called with a message' do
40
+ it 'does not mutate the input' do
41
+ canary = "'hello, world!'"
42
+ baseline = canary.dup
43
+ MyClass.log.info(canary)
44
+ assert_equal(baseline, canary)
45
+ end
46
+ end
47
+
48
+ describe 'layout' do
49
+ before do
50
+ @layout = MyClass::Layout.new
51
+ end
52
+
53
+ def layout(opts)
54
+ event = Critic::Fake::Event.new(opts)
55
+ formatted = @layout.format(event)
56
+
57
+ # Everything should end with a newline, but they're annoying
58
+ # to have to test elsewhere, so strip it away.
59
+ assert_equal("\n", formatted[-1], "Layout did not end with a newline: #{formatted.inspect}")
60
+ formatted.chomp
61
+ end
62
+
63
+ it 'log entry from info' do
64
+ rendered = layout(data: ["A Message", {:key1 => "ValueOne", :key2 => ["An", "Array"]}])
65
+ assert_equal('[9973] A Message: key1=ValueOne key2=["An","Array"]', rendered)
66
+ end
67
+
68
+ it 'logs the action_id correctly' do
69
+ LSpace.with(action_id: 'action') do
70
+ rendered = layout(data: ["A Message", {:key1 => "ValueOne", :key2 => ["An", "Array"]}])
71
+ assert_equal('[9973|action] A Message: key1=ValueOne key2=["An","Array"]', rendered)
72
+ end
73
+ end
74
+
75
+ it 'logs timestamp correctly' do
76
+ enable_timestamp
77
+ LSpace.with(action_id: 'action') do
78
+ rendered = layout(data: ["A Message", {:key1 => "ValueOne", :key2 => ["An", "Array"]}])
79
+ assert_equal('[1979-04-09 00:00:00.000000] [9973|action] A Message: key1=ValueOne key2=["An","Array"]', rendered)
80
+ end
81
+ end
82
+
83
+ it 'logs without pid correctly' do
84
+ disable_pid
85
+ LSpace.with(action_id: 'action') do
86
+ rendered = layout(data: ["A Message", {:key1 => "ValueOne", :key2 => ["An", "Array"]}])
87
+ assert_equal('[action] A Message: key1=ValueOne key2=["An","Array"]', rendered)
88
+ end
89
+ end
90
+
91
+ it 'log from info hash without a message' do
92
+ rendered = layout(data: [{:key1 => "ValueOne", :key2 => ["An", "Array"]}])
93
+ assert_equal('[9973] key1=ValueOne key2=["An","Array"]', rendered)
94
+ end
95
+
96
+ it 'renders [no backtrace] as appropriate' do
97
+ rendered = layout(data: ["Another Message", StandardError.new('msg')])
98
+ assert_equal("[9973] Another Message: error_class=StandardError error=msg\n[9973] [no backtrace]", rendered)
99
+ end
100
+
101
+ it 'renders when given error and info hash' do
102
+ rendered = layout(data: ["Another Message", StandardError.new('msg'), {:key1 => "ValueOne", :key2 => ["An", "Array"]}])
103
+ assert_equal(%Q{[9973] Another Message: key1=ValueOne key2=["An","Array"] error_class=StandardError error=msg\n[9973] [no backtrace]}, rendered)
104
+ end
105
+
106
+ it 'renders an error with a backtrace' do
107
+ error = StandardError.new('msg')
108
+ backtrace = ["a fake", "backtrace"]
109
+ error.set_backtrace(backtrace)
110
+
111
+ rendered = layout(data: ["Yet Another Message", error])
112
+ assert_equal("[9973] Yet Another Message: error_class=StandardError error=msg\n[9973] a fake\n[9973] backtrace", rendered)
113
+ end
114
+
115
+ it 'renders an error passed alone' do
116
+ error = StandardError.new('msg')
117
+ backtrace = ["a fake", "backtrace"]
118
+ error.set_backtrace(backtrace)
119
+
120
+ rendered = layout(data: [error])
121
+ assert_equal("[9973] error_class=StandardError error=msg\n[9973] a fake\n[9973] backtrace", rendered)
122
+ end
123
+
124
+ it 'handles bad unicode' do
125
+ rendered = layout(data: [{:key1 => "ValueOne", :key2 => "\xC3"}])
126
+ assert_equal("[9973] key1=ValueOne key2=\"\\xC3\" [JSON-FAILED]", rendered)
127
+ end
128
+
129
+ it 'allows disabling tagging' do
130
+ enable_timestamp
131
+ disable_tagging
132
+
133
+ LSpace.with(action_id: 'action') do
134
+ rendered = layout(data: [{:key1 => "ValueOne", :key2 => "Value Two"}])
135
+ assert_equal(%Q{key1=ValueOne key2="Value Two"}, rendered)
136
+ end
137
+ end
138
+
139
+ describe 'faults' do
140
+ it 'shows an appropriate error if the invalid arguments are provided' do
141
+ rendered = layout(data: ['foo', nil])
142
+
143
+ lines = rendered.split("\n")
144
+ assert_equal('[Chalk::Log fault: Could not format message] error_class="Chalk::Log::InvalidArguments" error="Invalid leftover arguments: [\"foo\", nil]"', lines[0])
145
+ assert(lines.length > 1)
146
+ end
147
+
148
+ it 'handles single faults' do
149
+ e = StandardError.new('msg')
150
+ @layout.expects(:do_format).raises(e)
151
+ rendered = layout(data: ['hi'])
152
+
153
+ lines = rendered.split("\n")
154
+ assert_equal('[Chalk::Log fault: Could not format message] error_class=StandardError error=msg', lines[0])
155
+ assert(lines.length > 1)
156
+ end
157
+
158
+ it 'handles double-faults' do
159
+ e = StandardError.new('msg')
160
+ def e.to_s; raise 'Time to double-fault'; end
161
+
162
+ @layout.expects(:do_format).raises(e)
163
+ rendered = layout(data: ['hi'])
164
+
165
+ lines = rendered.split("\n")
166
+ assert_match(/Chalk::Log fault: Double fault while formatting message/, lines[0])
167
+ assert_equal(1, lines.length, "Lines: #{lines.inspect}")
168
+ end
169
+
170
+ it 'handles triple-faults' do
171
+ e = StandardError.new('msg')
172
+ def e.to_s
173
+ f = StandardError.new('double')
174
+ def f.to_s; raise 'Time to triple fault'; end
175
+ raise f
176
+ end
177
+
178
+ @layout.expects(:do_format).raises(e)
179
+ rendered = layout(data: ['hi'])
180
+
181
+ lines = rendered.split("\n")
182
+ assert_match(/Chalk::Log fault: Triple fault while formatting message/, lines[0])
183
+ assert_equal(1, lines.length, "Lines: #{lines.inspect}")
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,296 @@
1
+ require File.expand_path('../_lib', __FILE__)
2
+
3
+ require 'chalk-log'
4
+
5
+ module Critic::Functional
6
+ class LogTest < Test
7
+ def self.add_appender
8
+ unless @appender
9
+ @appender = ::Logging.appenders.string_io(
10
+ 'test-stringio',
11
+ layout: Chalk::Log.layout
12
+ )
13
+ ::Logging.logger.root.add_appenders(@appender)
14
+ end
15
+ @appender
16
+ end
17
+
18
+ before do
19
+ Chalk::Log.init
20
+ end
21
+
22
+ describe 'when a class has included Log' do
23
+ it 'instances are loggable' do
24
+ class MyClass
25
+ include Chalk::Log
26
+ end
27
+
28
+ MyClass.new.log.info("Hi!")
29
+ end
30
+
31
+ it 'the class is loggable' do
32
+ class YourClass
33
+ include Chalk::Log
34
+ end
35
+
36
+ YourClass.log.info("Hi!")
37
+ end
38
+ end
39
+
40
+ describe 'including a loggable module into another' do
41
+ describe 'the inclusions are straightline' do
42
+ it 'make the includee loggable' do
43
+ module LogTestA
44
+ include Chalk::Log
45
+ end
46
+
47
+ module LogTestB
48
+ include LogTestA
49
+ end
50
+
51
+ assert(LogTestB < Chalk::Log)
52
+ assert(LogTestB.respond_to?(:log))
53
+ end
54
+
55
+ it 'preserves any custom include logic prior to Log inclusion' do
56
+ module CustomLogTestA
57
+ def self.dict=(dict)
58
+ @dict = dict
59
+ end
60
+
61
+ def self.dict
62
+ @dict
63
+ end
64
+
65
+ def self.included(other)
66
+ dict['included'] = true
67
+ end
68
+
69
+ include Chalk::Log
70
+ end
71
+
72
+ dict = {}
73
+ CustomLogTestA.dict = dict
74
+
75
+ module CustomLogTestB
76
+ include CustomLogTestA
77
+ end
78
+
79
+ assert(CustomLogTestB < Chalk::Log)
80
+ assert(CustomLogTestB.respond_to?(:log))
81
+ assert_equal(true, dict['included'])
82
+ end
83
+
84
+ # TODO: it'd be nice if this weren't true, but I'm not sure
85
+ # how to get a hook when a method is overriden.
86
+ it 'custom include logic after Log inclusion clobbers the default include logic' do
87
+ module CustomLogTestC
88
+ def self.dict=(dict)
89
+ @dict = dict
90
+ end
91
+
92
+ def self.dict
93
+ @dict
94
+ end
95
+
96
+ include Chalk::Log
97
+
98
+ def self.included(other)
99
+ dict['included'] = true
100
+ end
101
+ end
102
+
103
+ dict = {}
104
+ CustomLogTestC.dict = dict
105
+
106
+ module CustomLogTestD
107
+ include CustomLogTestC
108
+ end
109
+
110
+ assert(CustomLogTestD < Chalk::Log)
111
+ assert(!CustomLogTestD.respond_to?(:log))
112
+ assert_equal(true, dict['included'])
113
+ end
114
+ end
115
+ end
116
+
117
+ describe 'extending a Log module into another' do
118
+ describe 'the inclusions are straightline' do
119
+ it 'make the extendee loggable' do
120
+ module ExtendLogTestA
121
+ include Chalk::Log
122
+ end
123
+
124
+ module ExtendLogTestB
125
+ extend ExtendLogTestA
126
+ end
127
+
128
+ assert(ExtendLogTestB < Chalk::Log)
129
+ assert(ExtendLogTestB.respond_to?(:log))
130
+ end
131
+ end
132
+ end
133
+
134
+ describe 'when a class is loggable' do
135
+ class MyLog
136
+ include Chalk::Log
137
+ end
138
+
139
+ it 'log.warn works' do
140
+ msg = 'msg'
141
+ # For some reason this isn't working:
142
+ MyLog.log.backend.expects(:warn).once
143
+ MyLog.log.warn(msg)
144
+ end
145
+
146
+ it 'log.info works' do
147
+ msg = 'msg'
148
+ MyLog.log.backend.expects(:info).once
149
+ MyLog.log.info(msg)
150
+ end
151
+
152
+ it 'accepts blocks' do
153
+ class LogTestE
154
+ include Chalk::Log
155
+ end
156
+ LogTestE.log.level = "INFO"
157
+
158
+ LogTestE.log.debug { assert(false, "DEBUG block called when at INFO level") }
159
+ called = false
160
+ LogTestE.log.info { called = true; "" }
161
+ assert(called, "INFO block not called at INFO level")
162
+ end
163
+ end
164
+
165
+ class TestLogInstanceMethods < Test
166
+ include Chalk::Log
167
+
168
+ before do
169
+ TestLogInstanceMethods.log.level = 'INFO'
170
+ Chalk::Log.init
171
+ @appender = LogTest.add_appender
172
+ end
173
+
174
+ def assert_logged(expected_string, *args)
175
+ assert_includes(@appender.sio.string, expected_string, *args)
176
+ end
177
+
178
+ it 'accepts blocks on instance methods' do
179
+ called = false
180
+ log.debug { assert(false, "DEBUG block called at INFO") }
181
+ log.info { called = true; "" }
182
+ assert(called, "INFO block not called at INFO level")
183
+ end
184
+
185
+ describe 'when contextual info is set' do
186
+
187
+ it 'adds contextual information to `.info` log lines' do
188
+ log.with_contextual_info(key: 'value') {log.info("message")}
189
+ assert_logged("key=value")
190
+ end
191
+
192
+ it 'nests contexts' do
193
+ log.with_contextual_info(top_key: "top_value") do
194
+ log.info("top message")
195
+ log.with_contextual_info(inner_key: "inner_value") do
196
+ log.info("inner message")
197
+ end
198
+ end
199
+ %w{top_key=top_value inner_key=inner_value}.each do |pair|
200
+ assert_logged(pair)
201
+ end
202
+ end
203
+
204
+ it 'requires a block' do
205
+ exn = assert_raises(ArgumentError) do
206
+ log.with_contextual_info(i_am_not: "passing a block")
207
+ end
208
+ assert_includes(exn.message, "Must pass a block")
209
+ end
210
+
211
+ it 'requires its argument must be a hash' do
212
+ exn = assert_raises(TypeError) do
213
+ log.with_contextual_info('not a hash') {}
214
+ end
215
+ assert_includes(exn.message, 'must be a Hash')
216
+ end
217
+ end
218
+ end
219
+
220
+ describe 'when chaining includes and extends' do
221
+ it 'correctly make the end class loggable' do
222
+ module Base1
223
+ include Chalk::Log
224
+ end
225
+
226
+ class Child1
227
+ extend Base1
228
+ end
229
+
230
+ Child1.log.info("Hello!")
231
+ assert(true)
232
+ end
233
+
234
+ it 'correctly make the end class loggable when chaining an include and extend' do
235
+ module Base2
236
+ include Chalk::Log
237
+ end
238
+
239
+ module Middle2
240
+ extend Base2
241
+ end
242
+
243
+ class Child2
244
+ include Middle2
245
+ end
246
+
247
+ Child2.log.info("Hello!")
248
+ assert(true)
249
+ end
250
+
251
+ it 'correctly make the end class loggable when chaining an extend and an extend' do
252
+ module Base3
253
+ include Chalk::Log
254
+ end
255
+
256
+ module Middle3
257
+ extend Base3
258
+ end
259
+
260
+ class Child3
261
+ extend Middle3
262
+ end
263
+
264
+ Child3.log.info("Hello!")
265
+ assert(true)
266
+ end
267
+
268
+ it 'correctly make the end class loggable when it has already included loggable' do
269
+ module Base4
270
+ include Chalk::Log
271
+ end
272
+
273
+ module Middle4
274
+ extend Base4
275
+ end
276
+
277
+ class Child4
278
+ include Chalk::Log
279
+ extend Middle4
280
+ end
281
+
282
+ Child4.log.info("Hello!")
283
+ assert(true)
284
+ end
285
+ end
286
+
287
+ it 'correctly makes a module loggable' do
288
+ module Base5
289
+ include Chalk::Log
290
+ end
291
+
292
+ Base5.log.info("Hello!")
293
+ assert(true)
294
+ end
295
+ end
296
+ end