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.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +18 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +193 -0
- data/Rakefile +12 -0
- data/lib/yeti_logger.rb +164 -0
- data/lib/yeti_logger/configuration.rb +14 -0
- data/lib/yeti_logger/constants.rb +5 -0
- data/lib/yeti_logger/message_formatters.rb +93 -0
- data/lib/yeti_logger/test_helper.rb +65 -0
- data/lib/yeti_logger/version.rb +3 -0
- data/lib/yeti_logger/wrapped_logger.rb +24 -0
- data/spec/lib/yeti_logger/message_formatters_spec.rb +143 -0
- data/spec/lib/yeti_logger/test_helper_spec.rb +140 -0
- data/spec/lib/yeti_logger/wrapped_logger_spec.rb +28 -0
- data/spec/lib/yeti_logger_spec.rb +218 -0
- data/spec/spec_helper.rb +30 -0
- data/yeti_logger.gemspec +28 -0
- metadata +156 -0
@@ -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,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
|