yeti_logger 3.0.0

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,14 @@
1
+ module YetiLogger
2
+
3
+ module Configuration
4
+ attr_accessor :logger
5
+
6
+ def configure(&block)
7
+ instance_eval &block
8
+ end
9
+
10
+ end
11
+
12
+ extend Configuration
13
+
14
+ end
@@ -0,0 +1,5 @@
1
+ module YetiLogger
2
+ DEFAULT_OBJ_ARG = Object.new
3
+ LEVELS = [ :fatal, :error, :warn, :info, :debug ]
4
+ NUM_LINES_OF_EXCEPTIONS = 50
5
+ end
@@ -0,0 +1,93 @@
1
+ # require 'logger'
2
+ # require 'yeti_logger/version'
3
+ # require 'yeti_logger/configuration'
4
+ # require 'active_support/core_ext/benchmark'
5
+ # require 'active_support/concern'
6
+ # require 'active_support/core_ext/object/blank'
7
+ # require 'active_support/core_ext/object/try'
8
+
9
+
10
+ # Helper class used to format messages for logging. These can be used
11
+ # directly, but are more convenient when used via YetiLogger
12
+ module YetiLogger::MessageFormatters
13
+ NUM_LINES_OF_EXCEPTIONS = 50
14
+
15
+ # Helper method used to build up a single log message string you can pass to
16
+ # the underlying logger implementation.
17
+ # @param klass [String] Name of the class you are logging on behalf
18
+ # @param obj [Object] Object to log, may be nil
19
+ # @param exception [Exception] Optional exception to include in the log message
20
+ # @return [String] to log
21
+ def self.build_log_message(klass, obj, exception = nil, backtrace_lines = NUM_LINES_OF_EXCEPTIONS)
22
+ msg = if obj.is_a?(Hash)
23
+ if exception
24
+ format_hash(obj.merge(exception_hash(exception, backtrace_lines)))
25
+ else
26
+ format_hash(obj)
27
+ end
28
+ elsif exception
29
+ "#{obj} Exception: #{exception.message} "\
30
+ "Error Class: #{exception.class.name} "\
31
+ "#{format_backtrace(exception, backtrace_lines)}"
32
+ else
33
+ obj
34
+ end
35
+ "#{klass}: #{msg}"
36
+ end
37
+
38
+ # Format a Hash into key=value pairs, separated by whitespace.
39
+ # TODO: support nested hashes by serializing to JSON?
40
+ #
41
+ # @param hash [Hash] Hash to serialize into a key=value string
42
+ # @return [String] string representation of hash
43
+ def self.format_hash(hash)
44
+ hash.map do |k, v|
45
+ "#{k}=#{quote_unquoted(v.to_s)}"
46
+ end.join(' ')
47
+ end
48
+
49
+ # Helper method to quote strings that need quoting (spaces in them, or
50
+ # embedded quotes) that have not already been quoted.
51
+ # @param str [String] string to quote if it has spaces within it
52
+ # @return [String] original string, or quoted version if necessary
53
+ def self.quote_unquoted(str)
54
+ if str && (!needs_quoting?(str) || quoted?(str))
55
+ str
56
+ else
57
+ str.inspect
58
+ end
59
+ end
60
+
61
+ def self.needs_quoting?(str)
62
+ str.index(' ') || str.index('"')
63
+ end
64
+ private_class_method :needs_quoting?
65
+
66
+ def self.quoted?(str)
67
+ str[0] == ?" && str[-1] == ?"
68
+ end
69
+ private_class_method :quoted?
70
+
71
+ # Create a hash with the exception message and backtrace. You can merge this
72
+ # into an existing hash if you're logging key=value pairs.
73
+ # @param exception [Exception] The Exception you want to create a hash for.
74
+ # @param lines [Integer] How many lines of the backtrace to keep.
75
+ # @return [Hash] Hash with exception details in it.
76
+ def self.exception_hash(exception, lines = 20)
77
+ {
78
+ :error => exception.message,
79
+ :error_class => exception.class.name,
80
+ :backtrace => format_backtrace(exception, lines)
81
+ }
82
+ end
83
+
84
+ # Format a backtrace by joining all lines into a single line and quoting the
85
+ # results. You can optionally specify how many lines to include.
86
+ # @param exception [Exception] The Exception you want to convert to a string
87
+ # @param lines [Integer] How many lines of the backtrace to keep.
88
+ # @return [String] String of the backtrace.
89
+ def self.format_backtrace(exception, lines = 20)
90
+ exception.try(:backtrace).try(:take, lines).try(:join, ', ').inspect
91
+ end
92
+
93
+ end
@@ -0,0 +1,65 @@
1
+ # Helper methods for tests that interact with the Logging system
2
+ #
3
+ module YetiLogger::TestHelper
4
+
5
+ # Execute a block within the context of a changed log level. This will ensure
6
+ # the level is returned to is original state upon exit.
7
+ def with_log_level(level = Logger::DEBUG)
8
+ orig_level = YetiLogger.logger.level
9
+ begin
10
+ YetiLogger.logger.level = level
11
+ yield
12
+ ensure
13
+ YetiLogger.logger.level = orig_level
14
+ end
15
+ end
16
+
17
+ # Execute a block and ensure that among all the log messages received, message
18
+ # is among them. Not as elegant as a should_log type of method, but I can't
19
+ # figure out how to inject that into the rspec framework so it's evaluated
20
+ # after the method is done being called like should_receive's are.
21
+ def expect_to_see_log_message(message, level = :debug, &block)
22
+ expect_to_see_log_messages([message], level, &block)
23
+ end
24
+
25
+ # Plural version of above
26
+ def expect_to_see_log_messages(messages, level = :debug, &block)
27
+ log_messages = []
28
+
29
+ allow(YetiLogger.logger).to receive(level) do |log_line|
30
+ log_messages << log_line
31
+ end
32
+
33
+ block.call
34
+
35
+ # There is no unstub in rspec 3, but the closest to that would be to
36
+ # continue to stub it, but defer to the original implementation.
37
+ allow(YetiLogger.logger).to receive(level).and_call_original
38
+
39
+ # Find each message, removing the first occurrence.
40
+ messages.each do |message|
41
+ if message.is_a?(Regexp)
42
+ found = log_messages.find do |log_message|
43
+ log_message =~ message
44
+ end
45
+ if found
46
+ log_messages.delete_at(log_messages.find_index(found))
47
+ else
48
+ fail "Should have found #{message.inspect} amongst #{log_messages.inspect}"
49
+ end
50
+ else
51
+ expect(log_messages).to include(message)
52
+ log_messages.delete_at(log_messages.find_index(message))
53
+ end
54
+ end
55
+ end
56
+
57
+ def should_log(level = :info)
58
+ expect(YetiLogger.logger).to(receive(level))
59
+ end
60
+
61
+ def should_not_log(level = :info)
62
+ expect(YetiLogger.logger).to_not(receive(level))
63
+ end
64
+
65
+ end
@@ -0,0 +1,3 @@
1
+ module YetiLogger
2
+ VERSION = "3.0.0"
3
+ end
@@ -0,0 +1,24 @@
1
+ module YetiLogger
2
+
3
+ # This class provides a wrapper around an instance of a class that includes
4
+ # YetiLogger.
5
+ #
6
+ # The wrapper responds to the standard Logger methods :info, :warn, :error,
7
+ # and :debug, and forwards to the YetiLogger methods.
8
+ class WrappedLogger
9
+
10
+ # @param obj [Object] An instance of a class that includes YetiLogger.
11
+ def initialize(obj)
12
+ @obj = obj
13
+ end
14
+
15
+ # Use metaprogramming to define methods on the instance for each
16
+ # log level that YetiLogger supports.
17
+ YetiLogger::LEVELS.each do |level|
18
+ define_method(level) do |*args, &block|
19
+ instance_variable_get(:@obj).send("log_#{level}", *args, &block)
20
+ end
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,143 @@
1
+ require 'spec_helper.rb'
2
+
3
+ # Class used for class and instance level testing
4
+ module Yucatan
5
+ class YellinYeti
6
+ include YetiLogger
7
+ end
8
+ end
9
+
10
+ describe YetiLogger::MessageFormatters do
11
+ let(:exception) do
12
+ begin
13
+ raise StandardError.new('fail!')
14
+ rescue => ex
15
+ ex
16
+ end
17
+ end
18
+ let(:exception_without_backtrace) do
19
+ StandardError.new('no-backtrace fail!')
20
+ end
21
+ let(:klass) do
22
+ Yucatan::YellinYeti.name
23
+ end
24
+
25
+ describe '.build_log_message' do
26
+ let(:klassname) { klass }
27
+ let(:obj) { 'foo bar' }
28
+ let(:hash) do
29
+ {
30
+ :k1 => 'value',
31
+ :k2 => 37
32
+ }
33
+ end
34
+
35
+ it 'dumps plain ole objects' do
36
+ expect(described_class.build_log_message(klass, obj)).
37
+ to eq("#{klassname}: foo bar")
38
+ end
39
+
40
+ it 'formats hashes' do
41
+ expect(described_class.build_log_message(klass, hash)).
42
+ to eq("#{klassname}: k1=value k2=37")
43
+ end
44
+
45
+ it 'appends on exceptions if they there' do
46
+ expect(described_class.build_log_message(klass, 'message', exception)).
47
+ to eq("#{klassname}: message Exception: fail! "\
48
+ "Error Class: StandardError "\
49
+ "#{exception.backtrace.take(50).join(', ').inspect}")
50
+ end
51
+
52
+ it 'appends an exception onto the hash' do
53
+ expect(described_class.build_log_message(klass, hash, exception)).
54
+ to eq("#{klassname}: k1=value k2=37 "\
55
+ "error=fail! "\
56
+ "error_class=StandardError "\
57
+ "backtrace=#{exception.backtrace.take(50).join(', ').inspect}")
58
+ end
59
+
60
+ it 'quotes values that need them.' do
61
+ hash[:quoted] = 'some value that is very long'
62
+ expect(described_class.build_log_message(klass, hash)).
63
+ to eq("#{klassname}: k1=value k2=37 "\
64
+ "quoted=\"some value that is very long\"")
65
+ end
66
+
67
+ end
68
+
69
+ describe '.exception_hash' do
70
+ it 'formats a hash with the exception details' do
71
+ expect(described_class.exception_hash(exception)).
72
+ to eq({
73
+ :error => 'fail!',
74
+ :error_class => 'StandardError',
75
+ :backtrace => exception.backtrace.take(20).join(', ').inspect
76
+ })
77
+ end
78
+
79
+ it 'can deal with an exception without a backtrace' do
80
+ expect(described_class.exception_hash(exception_without_backtrace)).
81
+ to eq({
82
+ :error => 'no-backtrace fail!',
83
+ :error_class => 'StandardError',
84
+ :backtrace => 'nil'
85
+ })
86
+ end
87
+
88
+ it 'can be told to produce shorter backtraces' do
89
+ expect(described_class.exception_hash(exception, 1)).
90
+ to eq({
91
+ :error => 'fail!',
92
+ :error_class => 'StandardError',
93
+ :backtrace => exception.backtrace.take(1).join(', ').inspect
94
+ })
95
+ end
96
+ end
97
+
98
+ describe '.format_backtrace' do
99
+
100
+ it 'formats backtraces on one line' do
101
+ expect(described_class.format_backtrace(exception)).
102
+ to eq(exception.backtrace.take(20).join(', ').inspect)
103
+ end
104
+
105
+ it 'deals with exceptions without backtraces' do
106
+ expect(described_class.format_backtrace(exception_without_backtrace)).
107
+ to eq('nil')
108
+ end
109
+
110
+ it 'can be told to produce shorter backtraces' do
111
+ expect(described_class.format_backtrace(exception, 1)).
112
+ to eq(exception.backtrace.take(1).join(', ').inspect)
113
+ end
114
+
115
+ end
116
+
117
+ describe '.quote_unquoted' do
118
+
119
+ it 'does not quote nil values' do
120
+ expect(described_class.quote_unquoted(nil)).to eq("nil")
121
+ end
122
+
123
+ it 'does not quote simple values' do
124
+ expect(described_class.quote_unquoted("hello")).to eq("hello")
125
+ end
126
+
127
+ it 'does quote values with spaces in them' do
128
+ expect(described_class.quote_unquoted("hello world")).
129
+ to eq("\"hello world\"")
130
+ end
131
+
132
+ it 'does quote values with quotes in them' do
133
+ expect(described_class.quote_unquoted("hello\"world")).
134
+ to eq("\"hello\\\"world\"")
135
+ end
136
+
137
+ it 'does not re-quote quoted values' do
138
+ expect(described_class.quote_unquoted("\"hello world\"")).
139
+ to eq("\"hello world\"")
140
+ end
141
+
142
+ end
143
+ end
@@ -0,0 +1,140 @@
1
+ require 'spec_helper'
2
+
3
+ class YetiLogger::TestLogger
4
+ include YetiLogger
5
+ end
6
+
7
+
8
+ describe YetiLogger::TestHelper do
9
+ include described_class
10
+
11
+ let(:instance) { YetiLogger::TestLogger.new }
12
+
13
+ before(:each) do
14
+ YetiLogger.logger.level = Logger::INFO
15
+ expect(YetiLogger.logger.level).to be > Logger::DEBUG
16
+ end
17
+
18
+ # NOTE: I'm using the block form here since YetiLogger does the level checking
19
+ # for blocks, rather than relying on the logger implementation.
20
+ describe '.with_log_level' do
21
+
22
+ context 'when not using with_log_level' do
23
+
24
+ it 'respects the log level' do
25
+ expect(YetiLogger.logger).to_not receive(:debug)
26
+ expect(YetiLogger.logger).to receive(:info).with('YetiLogger::TestLogger: info')
27
+
28
+ instance.log_debug { 'debug' }
29
+ instance.log_info { 'info' }
30
+ end
31
+
32
+ it 'can adjust the log level to log more' do
33
+ expect(YetiLogger.logger).to receive(:debug).with('YetiLogger::TestLogger: say')
34
+ with_log_level(Logger::DEBUG) do
35
+ instance.log_debug { 'say' }
36
+ end
37
+ end
38
+
39
+ it 'can adjust the log level to log less' do
40
+ expect(YetiLogger.logger).to_not receive(:warn)
41
+ with_log_level(Logger::ERROR) do
42
+ instance.log_warn { 'warn!' }
43
+ end
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+
50
+ describe '.expect_to_see_log_messages' do
51
+ it 'has a singular form' do
52
+ expect_to_see_log_message('YetiLogger::TestLogger: foo', :warn) do
53
+ instance.log_warn('foo')
54
+ end
55
+ end
56
+
57
+ it 'checks for multiple messages' do
58
+ messages = [
59
+ 'YetiLogger::TestLogger: one',
60
+ 'YetiLogger::TestLogger: two'
61
+ ]
62
+ expect_to_see_log_messages(messages, :info) do
63
+ instance.log_info('one')
64
+ instance.log_info('two')
65
+ end
66
+ end
67
+
68
+ it 'only stubs the log level you request' do
69
+ expect(YetiLogger.logger).to receive(:info).with('YetiLogger::TestLogger: info')
70
+
71
+ expect_to_see_log_message('YetiLogger::TestLogger: warn', :warn) do
72
+ instance.log_info { 'info' }
73
+ instance.log_warn { 'warn' }
74
+ end
75
+ end
76
+
77
+ it 'supports regexes' do
78
+ messages = [
79
+ 'YetiLogger::TestLogger: one',
80
+ /two-\d/,
81
+ 'YetiLogger::TestLogger: three'
82
+ ]
83
+ expect_to_see_log_messages(messages, :info) do
84
+ instance.log_info('one')
85
+ instance.log_info('two-7')
86
+ instance.log_info('three')
87
+ end
88
+ end
89
+
90
+ it 'fails when it cannot find the string message' do
91
+ expect do
92
+ expect_to_see_log_message('not there', :info) do
93
+ instance.log_info('one')
94
+ end
95
+ end.to raise_exception(RSpec::Expectations::ExpectationNotMetError)
96
+ end
97
+
98
+ it 'fails when it cannot find the regexp message' do
99
+ expect do
100
+ expect_to_see_log_message(/bazinga/, :info) do
101
+ instance.log_info('one')
102
+ end
103
+ end.to raise_exception(RuntimeError)
104
+ end
105
+
106
+ it 'fails when it runs out of messages to search through' do
107
+ expect do
108
+ messages = [
109
+ 'YetiLogger::TestLogger: one',
110
+ /two-\d/,
111
+ 'YetiLogger::TestLogger: three',
112
+ 'YetiLogger::TestLogger: four'
113
+ ]
114
+ expect_to_see_log_messages(messages, :info) do
115
+ instance.log_info('one')
116
+ instance.log_info('two-7')
117
+ instance.log_info('three')
118
+ end
119
+ end.to raise_exception(RSpec::Expectations::ExpectationNotMetError)
120
+ end
121
+
122
+ end
123
+
124
+ describe '.should_log' do
125
+ it 'verifies a log message came through' do
126
+ should_log(:info).with("YetiLogger::TestLogger: hello!")
127
+ instance.log_info("hello!")
128
+ end
129
+ end
130
+
131
+ describe '.should_not_log' do
132
+ it 'verifies a log message does not happen' do
133
+ should_not_log(:info)
134
+ if false
135
+ instance.log_info("hello!")
136
+ end
137
+ end
138
+ end
139
+
140
+ end