traceable 1.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/lib/traceable/class_methods.rb +64 -0
- data/lib/traceable/config.rb +25 -0
- data/lib/traceable/tracer.rb +137 -0
- data/lib/traceable/version.rb +5 -0
- data/lib/traceable.rb +59 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/traceable_spec.rb +327 -0
- metadata +151 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: dc0fb1daa3bcf7f643a76c12767cb4b6879de019
|
4
|
+
data.tar.gz: a477c637eefd5b76b28105a83279d7faa5880650
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3384130f5711e0c854c9b00d093b82cca25e8b9f7333c2be73b61d58eb741df9abab464ed07a5cbd3f50dafbaeee8b11aa062a1a9ef20d19699e5c06b9c883aa
|
7
|
+
data.tar.gz: 34a461846b43e0015c1e5c67b53eeb306233729270283f5c546d2e42140930bf54d4472b7726cb31b8502829c935e55f16be910ec7df039f7109f5215634634a
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Traceable
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
# Put an automatic '#trace' call around the specified method,
|
10
|
+
# so that entry and exit from the method will be automatically logged.
|
11
|
+
#
|
12
|
+
# Can be used when defining the method:
|
13
|
+
#
|
14
|
+
# class X
|
15
|
+
# include SISApp::Traceable
|
16
|
+
# traced_method def foo
|
17
|
+
# ...
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# or it can be called after defining the method:
|
22
|
+
#
|
23
|
+
# class X
|
24
|
+
# include SISApp::Traceable
|
25
|
+
# def foo
|
26
|
+
# ...
|
27
|
+
# end
|
28
|
+
# traced_method :foo
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# The generated log message(s) will include the class name and method name
|
32
|
+
def traced_method(method_name)
|
33
|
+
klass = self
|
34
|
+
trace_name = "#{klass.name}##{method_name}"
|
35
|
+
orig_method = klass.instance_method(method_name)
|
36
|
+
|
37
|
+
klass.send(:define_method, method_name) do |*args, &block|
|
38
|
+
trace trace_name do
|
39
|
+
if block
|
40
|
+
orig_method.bind(self).call(*args) { |*block_args| block.call(*block_args) }
|
41
|
+
else
|
42
|
+
orig_method.bind(self).call(*args)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
method_name
|
48
|
+
end
|
49
|
+
|
50
|
+
# For the (relatively rare) case of calling trace() in a class method
|
51
|
+
# of a Traceable class - creates a new Tracer instance
|
52
|
+
def trace(msg = nil, **tags)
|
53
|
+
tracer = Tracer.new(Tracer.default_parent)
|
54
|
+
|
55
|
+
if block_given?
|
56
|
+
tracer.do_block(msg, **tags) { yield }
|
57
|
+
elsif msg
|
58
|
+
tracer.info msg, **tags
|
59
|
+
else
|
60
|
+
tracer
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Traceable
|
6
|
+
class Config
|
7
|
+
attr_accessor :default_tags
|
8
|
+
attr_accessor :logger
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@logger = Logger.new(STDOUT)
|
12
|
+
@logger.level = Logger::INFO
|
13
|
+
|
14
|
+
@default_tags = {}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.configure(&_)
|
19
|
+
yield config
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.config
|
23
|
+
@config ||= Config.new
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Traceable
|
4
|
+
# An instance of this class is used by each Traceable object to
|
5
|
+
# generate log entries. Each log entry consists of a key/value
|
6
|
+
# map ("tags"). Tags will be inherited from a parent instance,
|
7
|
+
# if provided.
|
8
|
+
class Tracer
|
9
|
+
attr_reader :logger
|
10
|
+
attr_reader :id
|
11
|
+
attr_reader :parent
|
12
|
+
attr_reader :tags
|
13
|
+
|
14
|
+
def initialize(parent_tracer, tags: nil)
|
15
|
+
@logger = Traceable.config.logger
|
16
|
+
|
17
|
+
@parent = case parent_tracer
|
18
|
+
when nil
|
19
|
+
nil
|
20
|
+
when Tracer
|
21
|
+
parent_tracer
|
22
|
+
when Traceable
|
23
|
+
parent_tracer.local_tracer
|
24
|
+
else
|
25
|
+
raise(ArgumentError, "#{parent_tracer} (#{parent_tracer.class})")
|
26
|
+
end
|
27
|
+
|
28
|
+
@tags = @parent ? @parent.tags.dup : Tracer.default_tags
|
29
|
+
@tags.merge!(tags) if tags
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.default_tags
|
33
|
+
tags = Traceable.config.default_tags.merge(
|
34
|
+
trace: SecureRandom.uuid
|
35
|
+
)
|
36
|
+
tags.each_key do |k|
|
37
|
+
value = tags[k]
|
38
|
+
tags[k] = value.call if value.respond_to?(:call)
|
39
|
+
end
|
40
|
+
tags
|
41
|
+
end
|
42
|
+
|
43
|
+
def emit(method, msg, **tags)
|
44
|
+
final_tags = make_tags(message: msg, **tags)
|
45
|
+
emit_tags(method, final_tags)
|
46
|
+
end
|
47
|
+
|
48
|
+
def make_tags(**tags)
|
49
|
+
@tags.merge(tags)
|
50
|
+
end
|
51
|
+
|
52
|
+
def emit_tags(method, tags)
|
53
|
+
logger.send(method, tags)
|
54
|
+
rescue StandardError => ex
|
55
|
+
warn "EXCEPTION in trace: #{ex}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def fatal(msg, **tags)
|
59
|
+
emit :fatal, msg, **tags
|
60
|
+
end
|
61
|
+
|
62
|
+
def error(msg, **tags)
|
63
|
+
emit :error, msg, **tags
|
64
|
+
end
|
65
|
+
|
66
|
+
def warn(msg, **tags)
|
67
|
+
emit :warn, msg, **tags
|
68
|
+
end
|
69
|
+
|
70
|
+
def info(msg, **tags)
|
71
|
+
emit :info, msg, **tags
|
72
|
+
end
|
73
|
+
|
74
|
+
def debug(msg, **tags)
|
75
|
+
emit :debug, msg, **tags
|
76
|
+
end
|
77
|
+
|
78
|
+
# rubocop:disable Metrics/AbcSize
|
79
|
+
# rubocop:disable Metrics/MethodLength
|
80
|
+
def do_block(msg, **tags, &_)
|
81
|
+
info "START: #{msg}", enter: true, **tags
|
82
|
+
block_start_time = Time.now.utc
|
83
|
+
|
84
|
+
begin
|
85
|
+
push
|
86
|
+
yield
|
87
|
+
rescue StandardError => ex
|
88
|
+
elapsed = Time.now.utc - block_start_time
|
89
|
+
if ex.instance_variable_defined?(:@traceable_rescued)
|
90
|
+
origin = ex.instance_variable_get(:@traceable_rescued)
|
91
|
+
ex_message = " [propagated from #{origin}]"
|
92
|
+
else
|
93
|
+
ex.instance_variable_set(:@traceable_rescued, msg)
|
94
|
+
ex_message = ", #{ex.message}"
|
95
|
+
tags[:backtrace] ||= ex.backtrace.join('\n')
|
96
|
+
end
|
97
|
+
|
98
|
+
warn "EXCEPTION: #{msg} => #{ex.class.name}#{ex_message}",
|
99
|
+
exception: true, elapsed: elapsed, class: ex.class.name, **tags
|
100
|
+
|
101
|
+
raise
|
102
|
+
ensure
|
103
|
+
pop
|
104
|
+
|
105
|
+
unless ex
|
106
|
+
elapsed = Time.now.utc - block_start_time
|
107
|
+
info "END: #{msg}", exit: true, elapsed: elapsed, **tags
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
# rubocop:enable Metrics/AbcSize
|
112
|
+
# rubocop:enable Metrics/MethodLength
|
113
|
+
|
114
|
+
def self.default_parent
|
115
|
+
tracer_stack.last # nil if nothing currently on the stack
|
116
|
+
end
|
117
|
+
|
118
|
+
# The tracer stack is a thread-local list of tracer instances, allowing the
|
119
|
+
# current tracer context to be automatically inherited by any other tracer
|
120
|
+
# instances created further down in the stack.
|
121
|
+
#
|
122
|
+
# The current tracer is pushed onto the stack when entering a traced block,
|
123
|
+
# then popped from the stack when leaving the traced block.
|
124
|
+
|
125
|
+
def self.tracer_stack
|
126
|
+
Thread.current[:tracer_stack] ||= []
|
127
|
+
end
|
128
|
+
|
129
|
+
def push
|
130
|
+
Tracer.tracer_stack.push self
|
131
|
+
end
|
132
|
+
|
133
|
+
def pop
|
134
|
+
Tracer.tracer_stack.pop
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
data/lib/traceable.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module Traceable
|
6
|
+
# A module for enhanced logging facilities.
|
7
|
+
#
|
8
|
+
# The intent of the Traceable functionality is to have log
|
9
|
+
# messages that provide runtime diagnostics as well as
|
10
|
+
# code documentation.
|
11
|
+
|
12
|
+
# Generate a log message
|
13
|
+
# When called without a block, generates a single log message:
|
14
|
+
#
|
15
|
+
# trace "this is a single message"
|
16
|
+
# trace.error "something bad happened"
|
17
|
+
#
|
18
|
+
# When called with a block, the given message is used to compose
|
19
|
+
# a log output at the entry and exit of the block.
|
20
|
+
#
|
21
|
+
# trace "doing something nifty" do
|
22
|
+
# do_something
|
23
|
+
# end
|
24
|
+
def trace(msg = nil, **tags)
|
25
|
+
tracer = local_tracer
|
26
|
+
|
27
|
+
if block_given?
|
28
|
+
tracer.do_block(msg, **tags) { yield }
|
29
|
+
elsif msg
|
30
|
+
tracer.info msg, **tags
|
31
|
+
else
|
32
|
+
tracer
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def local_tracer
|
37
|
+
@tracer ||= init_tracer
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create the tracer instance used for generating log messages.
|
41
|
+
# If a parent is givent, tags and other settings will be
|
42
|
+
# inherited from it. If a parent is not given, it will automatically
|
43
|
+
# inherit from the next highest tracer in the call stack, if any.
|
44
|
+
#
|
45
|
+
# The default set of tags includes a unique ID string, so all
|
46
|
+
# log messages generated from that tracer will have the same
|
47
|
+
# ID string. In combination with auto-inheriting from a parent
|
48
|
+
# tracer, this means that all tracing messages starting from
|
49
|
+
# some common root will have the same ID string to be able
|
50
|
+
# to group together related messages in the log.
|
51
|
+
def init_tracer(parent: nil, tags: nil)
|
52
|
+
parent ||= Tracer.default_parent
|
53
|
+
@tracer = Tracer.new(parent, tags: tags)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
require 'traceable/class_methods'
|
58
|
+
require 'traceable/config'
|
59
|
+
require 'traceable/tracer'
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
if /^2\.4/ =~ RUBY_VERSION # Limit coverage to one build
|
4
|
+
require 'simplecov'
|
5
|
+
|
6
|
+
SimpleCov.start do
|
7
|
+
add_filter 'lib/traceable/version.rb'
|
8
|
+
add_filter 'spec'
|
9
|
+
track_files 'lib/**/*.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
SimpleCov.minimum_coverage(100)
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'traceable'
|
16
|
+
|
17
|
+
require 'byebug'
|
18
|
+
require 'pry'
|
19
|
+
|
20
|
+
RSpec.configure do |config|
|
21
|
+
config.expect_with(:rspec) do |c|
|
22
|
+
c.syntax = %i[should expect]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
26
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
27
|
+
# test failures related to randomization by passing the same `--seed` value
|
28
|
+
# as the one that triggered the failure.
|
29
|
+
config.order = :random
|
30
|
+
Kernel.srand config.seed
|
31
|
+
end
|
@@ -0,0 +1,327 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Traceable do
|
4
|
+
class FakeLogger
|
5
|
+
attr_reader :logs
|
6
|
+
def initialize
|
7
|
+
@logs = []
|
8
|
+
end
|
9
|
+
|
10
|
+
%i[info debug warn error fatal].each do |method|
|
11
|
+
define_method(method) do |tags|
|
12
|
+
logs << { method: method, tags: tags }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:fake_logger) { FakeLogger.new }
|
18
|
+
let(:logs) { fake_logger.logs }
|
19
|
+
|
20
|
+
before do
|
21
|
+
allow_any_instance_of(Traceable::Tracer).to receive(:logger) { fake_logger }
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:subject_class) do
|
25
|
+
Class.new do
|
26
|
+
include Traceable
|
27
|
+
|
28
|
+
attr_reader :non_traced_called
|
29
|
+
def a_non_traced_method
|
30
|
+
@non_traced_called = true
|
31
|
+
:non_traced_return_value
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :traced_called
|
35
|
+
traced_method def a_traced_method
|
36
|
+
@traced_called = true
|
37
|
+
yield if block_given?
|
38
|
+
:traced_return_value
|
39
|
+
end
|
40
|
+
|
41
|
+
traced_method def a_traced_method_that_calls_a_block_with_a_parameter(val)
|
42
|
+
yield val
|
43
|
+
end
|
44
|
+
|
45
|
+
def trace_as_a_block
|
46
|
+
trace 'an example' do
|
47
|
+
trace 'a nested block' do
|
48
|
+
true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.a_class_method
|
54
|
+
trace 'in a class method'
|
55
|
+
trace 'in a class method with a block' do
|
56
|
+
trace.info 'test'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
let(:contained_class) do
|
63
|
+
Class.new do
|
64
|
+
include Traceable
|
65
|
+
|
66
|
+
def initialize(parent)
|
67
|
+
init_tracer(parent: parent, tags: { inside: true })
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
subject do
|
73
|
+
subject_class.new
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '#configure' do
|
77
|
+
it 'receives a Config instance' do
|
78
|
+
@config = nil
|
79
|
+
Traceable.configure { |c| @config = c }
|
80
|
+
expect(@config).to be_a Traceable::Config
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe '#config' do
|
85
|
+
end
|
86
|
+
|
87
|
+
describe '#trace' do
|
88
|
+
it 'responds to #trace' do
|
89
|
+
expect(subject.respond_to?(:trace)).to eq(true)
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'when no block is given' do
|
93
|
+
it 'can be used like an explicit logger' do
|
94
|
+
subject.trace.info 'general info'
|
95
|
+
subject.trace.warn 'a warning'
|
96
|
+
subject.trace.error 'things look bad'
|
97
|
+
subject.trace.fatal 'fell on the floor'
|
98
|
+
subject.trace.debug 'fix me'
|
99
|
+
expect(logs[0][:method]).to eq(:info)
|
100
|
+
expect(logs[1][:method]).to eq(:warn)
|
101
|
+
expect(logs[2][:method]).to eq(:error)
|
102
|
+
expect(logs[3][:method]).to eq(:fatal)
|
103
|
+
expect(logs[4][:method]).to eq(:debug)
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'logs as info as default' do
|
107
|
+
subject.trace 'blah', foo: :bar
|
108
|
+
expect(logs.size).to be(1)
|
109
|
+
expect(logs[0][:method]).to eq(:info)
|
110
|
+
expect(logs[0][:tags][:foo]).to be(:bar)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'when a block is given' do
|
115
|
+
it 'logs at start and end of the block' do
|
116
|
+
subject.trace_as_a_block
|
117
|
+
expect(logs.size).to be(4)
|
118
|
+
expect(logs[0][:tags].key?(:enter)).to be(true)
|
119
|
+
expect(logs[0][:tags][:message]).to eq('START: an example')
|
120
|
+
expect(logs[1][:tags].key?(:enter)).to be(true)
|
121
|
+
expect(logs[1][:tags][:message]).to eq('START: a nested block')
|
122
|
+
expect(logs[2][:tags].key?(:exit)).to be(true)
|
123
|
+
expect(logs[2][:tags][:message]).to eq('END: a nested block')
|
124
|
+
expect(logs[3][:tags].key?(:exit)).to be(true)
|
125
|
+
expect(logs[3][:tags][:message]).to eq('END: an example')
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'reports the elapsed time of the block' do
|
129
|
+
subject.trace_as_a_block
|
130
|
+
count = 0
|
131
|
+
logs.select { |log| log[:tags].key?(:exit) }.each do |log|
|
132
|
+
expect(log[:tags][:elapsed]).to be_a(Numeric)
|
133
|
+
count += 1
|
134
|
+
end
|
135
|
+
expect(count).to be(2)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context 'in a class method' do
|
140
|
+
it 'works just like an instance method' do
|
141
|
+
expect { subject.class.a_class_method }.to change { logs.size }.by 4
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe '#local_tracer' do
|
147
|
+
it 'returns a Tracer object' do
|
148
|
+
expect(subject.local_tracer).to be_a(Traceable::Tracer)
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'inherits a parent tracer' do
|
152
|
+
contained = contained_class.new(subject)
|
153
|
+
expect(contained.local_tracer).not_to eq(subject.local_tracer)
|
154
|
+
expect(contained.local_tracer.parent).to eq(subject.local_tracer)
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'adds tags from the child' do
|
158
|
+
contained = contained_class.new(subject)
|
159
|
+
expect(subject.local_tracer.tags.key?(:inside)).to be(false)
|
160
|
+
expect(contained.local_tracer.tags.key?(:inside)).to be(true)
|
161
|
+
end
|
162
|
+
|
163
|
+
context 'tracer IDs' do
|
164
|
+
class Foo
|
165
|
+
include Traceable
|
166
|
+
def do_it
|
167
|
+
trace 'outside' do
|
168
|
+
trace 'inside' do
|
169
|
+
trace 'a message'
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'creates a unique ID each time' do
|
176
|
+
(1..10).each do |_|
|
177
|
+
Foo.new.do_it
|
178
|
+
end
|
179
|
+
|
180
|
+
ids = Set.new
|
181
|
+
logs.each { |log| ids << log[:tags][:trace] }
|
182
|
+
expect(ids.size).to eq(10)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe '.traced_method' do
|
188
|
+
context 'for non-traced methods' do
|
189
|
+
it 'does not interfere' do
|
190
|
+
expect(subject.a_non_traced_method).to eq(:non_traced_return_value)
|
191
|
+
expect(subject.non_traced_called).to eq(true)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
context 'for traced methods' do
|
196
|
+
it 'does not interfere' do
|
197
|
+
expect(subject.a_traced_method).to eq(:traced_return_value)
|
198
|
+
expect(subject.traced_called).to eq(true)
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'emits a log entry at start and end' do
|
202
|
+
subject.a_traced_method
|
203
|
+
expect(logs.size).to be(2)
|
204
|
+
expect(logs[0][:tags].key?(:enter)).to be(true)
|
205
|
+
expect(logs[1][:tags].key?(:exit)).to be(true)
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'catches and raises exceptions' do
|
209
|
+
expect { subject.a_traced_method { raise 'oops' } }.to raise_error('oops')
|
210
|
+
expect(logs.size).to be(2)
|
211
|
+
expect(logs[0][:tags].key?(:enter)).to be(true)
|
212
|
+
expect(logs[1][:tags].key?(:exception)).to be(true)
|
213
|
+
expect(logs[1][:tags].key?(:elapsed)).to be(true)
|
214
|
+
expect(logs[1][:tags].key?(:backtrace)).to be(true)
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'catches and raises exceptions through nested blocks' do
|
218
|
+
expect do
|
219
|
+
subject.trace 'level one' do
|
220
|
+
subject.a_traced_method do
|
221
|
+
subject.trace 'level two' do
|
222
|
+
raise 'oops'
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end.to raise_error('oops')
|
227
|
+
|
228
|
+
origin = logs.find { |log| log[:tags][:exception] && log[:tags][:message].include?('level two') }
|
229
|
+
expect(origin[:tags].key?(:backtrace)).to be(true)
|
230
|
+
|
231
|
+
middle = logs.find { |log| log[:tags][:exception] && log[:tags][:message].include?('a_traced_method') }
|
232
|
+
expect(middle[:tags].key?(:backtrace)).to be(false)
|
233
|
+
expect(middle[:tags][:message].include?('propagated')).to be(true)
|
234
|
+
|
235
|
+
outer = logs.find { |log| log[:tags][:exception] && log[:tags][:message].include?('level one') }
|
236
|
+
expect(outer[:tags].key?(:backtrace)).to be(false)
|
237
|
+
expect(middle[:tags][:message].include?('propagated')).to be(true)
|
238
|
+
end
|
239
|
+
|
240
|
+
it 'properly passes a block through' do
|
241
|
+
yielded = false
|
242
|
+
subject.a_traced_method do
|
243
|
+
yielded = true
|
244
|
+
end
|
245
|
+
expect(yielded).to eq(true)
|
246
|
+
end
|
247
|
+
|
248
|
+
it 'properly passes a block with args through' do
|
249
|
+
yielded = false
|
250
|
+
subject.a_traced_method_that_calls_a_block_with_a_parameter(:blah) do |val|
|
251
|
+
yielded = val
|
252
|
+
end
|
253
|
+
expect(yielded).to eq(:blah)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
describe Traceable::Tracer do
|
259
|
+
describe '#default_parent' do
|
260
|
+
class One
|
261
|
+
include Traceable
|
262
|
+
def initialize
|
263
|
+
init_tracer tags: { one: true }
|
264
|
+
end
|
265
|
+
|
266
|
+
traced_method def first
|
267
|
+
Two.new.second
|
268
|
+
Thread.new { Two.new.second }.join # New thread --> don't inherit tracer context
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
class Two
|
273
|
+
def second
|
274
|
+
Three.new.third
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
class Three
|
279
|
+
include Traceable
|
280
|
+
traced_method def third
|
281
|
+
trace 'in the third', three: true
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
it 'should include tags from the default parent' do
|
286
|
+
One.new.first
|
287
|
+
# expected logs:
|
288
|
+
# 0 START: One#first, :one => true
|
289
|
+
# 1 START: Three#third, :one => true, :three => true
|
290
|
+
# 2 'in the third', :one => true, :three => true
|
291
|
+
# 3 END: Three#third
|
292
|
+
# 4 START: Three#third, :three => true
|
293
|
+
# 5 'in the third', :three => true
|
294
|
+
# 6 END: Three#third
|
295
|
+
# 7 END: One#first, :one => true
|
296
|
+
expect(logs.count).to be(8)
|
297
|
+
|
298
|
+
expect(logs[1][:tags].key?(:one)).to be(true)
|
299
|
+
expect(logs[2][:tags].key?(:one)).to be(true)
|
300
|
+
expect(logs[1][:tags][:trace]).to eq(logs[0][:tags][:trace]) # Same parent, same trace tag
|
301
|
+
|
302
|
+
expect(logs[4][:tags].key?(:one)).to be(false)
|
303
|
+
expect(logs[5][:tags].key?(:one)).to be(false)
|
304
|
+
expect(logs[4][:tags][:trace]).to_not eq(logs[1][:tags][:trace]) # Diff parent, diff trace tag
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
describe '#initialize' do
|
309
|
+
it 'complains with invalid argument' do
|
310
|
+
expect { Traceable::Tracer.new('foo') }.to raise_error(ArgumentError)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
describe '#emit_tags' do
|
315
|
+
context 'when logger chokes on the message' do
|
316
|
+
before do
|
317
|
+
allow(fake_logger).to receive(:info) { raise('blow chunks') }
|
318
|
+
end
|
319
|
+
it 'does not allow the exception to blow up' do
|
320
|
+
t = Traceable::Tracer.new(nil)
|
321
|
+
expect(t).to receive(:emit_tags).twice.and_call_original
|
322
|
+
t.info(message: 'just a test')
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
metadata
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: traceable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeremy Slade
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-02-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: byebug
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.4.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 3.4.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: simplecov
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.14'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.14'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: wwtd
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 1.3.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 1.3.0
|
111
|
+
description:
|
112
|
+
email:
|
113
|
+
- jeremy@jkslade.net
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- lib/traceable.rb
|
119
|
+
- lib/traceable/class_methods.rb
|
120
|
+
- lib/traceable/config.rb
|
121
|
+
- lib/traceable/tracer.rb
|
122
|
+
- lib/traceable/version.rb
|
123
|
+
- spec/spec_helper.rb
|
124
|
+
- spec/traceable_spec.rb
|
125
|
+
homepage: https://github.com/instructure/inst-jobs-statsd
|
126
|
+
licenses:
|
127
|
+
- MIT
|
128
|
+
metadata: {}
|
129
|
+
post_install_message:
|
130
|
+
rdoc_options: []
|
131
|
+
require_paths:
|
132
|
+
- lib
|
133
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '2.3'
|
138
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - ">="
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
requirements: []
|
144
|
+
rubyforge_project:
|
145
|
+
rubygems_version: 2.5.1
|
146
|
+
signing_key:
|
147
|
+
specification_version: 4
|
148
|
+
summary: Instrument code with logging
|
149
|
+
test_files:
|
150
|
+
- spec/spec_helper.rb
|
151
|
+
- spec/traceable_spec.rb
|