traceable 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|