startback 0.17.3 → 0.18.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/startback/audit/ext/context.rb +35 -0
- data/lib/startback/audit/ext.rb +1 -0
- data/lib/startback/audit/middleware.rb +30 -0
- data/lib/startback/audit/null_tracer.rb +31 -0
- data/lib/startback/audit/operation_tracer.rb +19 -0
- data/lib/startback/audit/shared.rb +18 -0
- data/lib/startback/audit/span.rb +66 -0
- data/lib/startback/audit/trace_logger.rb +30 -0
- data/lib/startback/audit/tracer.rb +85 -0
- data/lib/startback/audit.rb +7 -1
- data/lib/startback/context/h_factory.rb +21 -0
- data/lib/startback/context.rb +1 -1
- data/lib/startback/event/ext/operation.rb +4 -5
- data/lib/startback/support/fake_logger.rb +16 -5
- data/lib/startback/support/log_formatter.rb +25 -2
- data/lib/startback/support/redactor.rb +40 -0
- data/lib/startback/support.rb +1 -0
- data/lib/startback/version.rb +2 -2
- data/spec/spec_helper.rb +1 -0
- data/spec/unit/audit/ext/test_context.rb +66 -0
- data/spec/unit/audit/test_middleware.rb +66 -0
- data/spec/unit/audit/test_trace_logger.rb +51 -0
- data/spec/unit/audit/test_tracer.rb +106 -0
- data/spec/unit/context/test_h_factory.rb +5 -3
- data/spec/unit/support/test_redactor.rb +84 -0
- metadata +20 -8
- data/lib/startback/audit/trailer.rb +0 -132
- data/spec/unit/audit/test_trailer.rb +0 -105
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7ae59b533182869f37a6a720314be714a3c348d294071724c7f6f4931385d1d5
|
4
|
+
data.tar.gz: de7630071838aacc2c2a916b8aeade7e39b9e580617b8dd89f56995dba995d68
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d2804d9ed39c41c8ad5d3a45b7b6e53d021fe3f20fbe30fa7ed5f08e8450ee23b69870879a044b5f971cd07fff821177233bbce71ae4edb7c4acc81bc9a7910c
|
7
|
+
data.tar.gz: 9e42c4b69121a35d01aafbd42c08d0fbde53b5cbca308036b24bd6a80a840f455925f33abae6d27abfd5df9f7f69222db961db0cb1e724a417320f1c83481ffb
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Startback
|
2
|
+
class Context
|
3
|
+
attr_accessor :tracer
|
4
|
+
|
5
|
+
def tracer
|
6
|
+
@tracer ||= Audit::Tracer.empty.on_span(
|
7
|
+
Audit::TraceLogger.new(logger)
|
8
|
+
)
|
9
|
+
end
|
10
|
+
|
11
|
+
def trace_span(attributes = {}, &block)
|
12
|
+
@tracer = tracer.new_trace unless tracer.attached?
|
13
|
+
tracer.fork(attributes, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
h_dump do |h|
|
17
|
+
next unless tracer.attached?
|
18
|
+
|
19
|
+
last_span = tracer.last_span!
|
20
|
+
h.merge!("tracing" => {
|
21
|
+
"trace_id" => last_span.trace_id,
|
22
|
+
"span_id" => last_span.span_id,
|
23
|
+
"parent_id" => last_span.parent_id,
|
24
|
+
})
|
25
|
+
end
|
26
|
+
|
27
|
+
h_factory do |c, h|
|
28
|
+
next unless h['tracing']
|
29
|
+
|
30
|
+
trace_id = h['tracing']['trace_id']
|
31
|
+
span_id = h['tracing']['span_id']
|
32
|
+
c.tracer = c.tracer.attach_to(trace_id, span_id)
|
33
|
+
end
|
34
|
+
end # class Context
|
35
|
+
end # module Startback
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'ext/context'
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Startback
|
2
|
+
module Audit
|
3
|
+
class Middleware
|
4
|
+
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
context = ::Startback::Context::Middleware.context(env)
|
11
|
+
|
12
|
+
# attach to the existing trace if any
|
13
|
+
trace_id = env['HTTP_X_TRACE_ID']
|
14
|
+
span_id = env['HTTP_X_SPAN_ID']
|
15
|
+
context.tracer = context.tracer.attach_to(trace_id, span_id) if trace_id && span_id
|
16
|
+
|
17
|
+
# trace it!
|
18
|
+
context.trace_span({
|
19
|
+
:type => :request_handler,
|
20
|
+
:method => env['REQUEST_METHOD'],
|
21
|
+
:path => env['PATH_INFO'],
|
22
|
+
:qs => env['QUERY_STRING']
|
23
|
+
}) do
|
24
|
+
@app.call(env)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end # class Middleware
|
29
|
+
end # module Audit
|
30
|
+
end # module Startback
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Startback
|
2
|
+
module Audit
|
3
|
+
class NullTracer
|
4
|
+
|
5
|
+
def attached?
|
6
|
+
false
|
7
|
+
end
|
8
|
+
|
9
|
+
def last_span!
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def new_trace(*args)
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def attach_to(*args)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def fork(*args)
|
22
|
+
yield
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_span(listener = nil, &block)
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
end # class NullTracer
|
30
|
+
end # module Audit
|
31
|
+
end # module Startback
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'startback/audit'
|
2
|
+
|
3
|
+
module Startback
|
4
|
+
module Audit
|
5
|
+
class OperationTracer
|
6
|
+
include Startback::Audit::Shared
|
7
|
+
|
8
|
+
def call(runner, op, &block)
|
9
|
+
op.context.trace_span({
|
10
|
+
type: :operation,
|
11
|
+
op: op_name(op),
|
12
|
+
data: op_data(op),
|
13
|
+
context: op_context(op)
|
14
|
+
}, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
end # class OperationTracer
|
18
|
+
end # module Audit
|
19
|
+
end # module Startback
|
@@ -12,6 +12,24 @@ module Startback
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
+
def op_context(op)
|
16
|
+
op.respond_to?(:context, false) ? op.context.to_h : {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def op_data(op)
|
20
|
+
if op.respond_to?(:op_data, false)
|
21
|
+
op.op_data
|
22
|
+
elsif op.respond_to?(:to_trail, false)
|
23
|
+
op.to_trail
|
24
|
+
elsif op.respond_to?(:input, false)
|
25
|
+
op.input
|
26
|
+
elsif op.respond_to?(:request, false)
|
27
|
+
op.request
|
28
|
+
elsif op.is_a?(Operation::MultiOperation)
|
29
|
+
op.ops.map{ |sub_op| op_to_trail(sub_op) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
15
33
|
end # module Shared
|
16
34
|
end # module Audit
|
17
35
|
end # module Startback
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Startback
|
2
|
+
module Audit
|
3
|
+
class Span
|
4
|
+
|
5
|
+
def initialize(trace_id, parent_id, attributes = {}, span_id = SecureRandom.uuid)
|
6
|
+
@trace_id, @parent_id = trace_id, parent_id
|
7
|
+
@span_id = span_id
|
8
|
+
@attributes = attributes
|
9
|
+
@status = 'unknown'
|
10
|
+
@at = (Time.now.to_f*1000).to_i
|
11
|
+
@timing = nil
|
12
|
+
@error = nil
|
13
|
+
end
|
14
|
+
attr_reader :trace_id, :parent_id, :span_id, :status, :attributes, :timing, :error
|
15
|
+
|
16
|
+
def finished?
|
17
|
+
@status != 'unknown'
|
18
|
+
end
|
19
|
+
|
20
|
+
def success?
|
21
|
+
@status == 'success'
|
22
|
+
end
|
23
|
+
|
24
|
+
def error?
|
25
|
+
@status == 'error'
|
26
|
+
end
|
27
|
+
|
28
|
+
def fork(attributes = {})
|
29
|
+
Span.new(@trace_id, @span_id, attributes)
|
30
|
+
end
|
31
|
+
|
32
|
+
def finish(timing, error = nil)
|
33
|
+
@timing = timing
|
34
|
+
@status = error ? 'error' : 'success'
|
35
|
+
@error = error
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_h
|
40
|
+
{
|
41
|
+
:spanId => span_id,
|
42
|
+
:traceId => trace_id,
|
43
|
+
:parentId => parent_id,
|
44
|
+
:status => status,
|
45
|
+
:timing => timing_to_h,
|
46
|
+
:attributes => attributes,
|
47
|
+
:error => error,
|
48
|
+
}.compact
|
49
|
+
end
|
50
|
+
|
51
|
+
def timing_to_h
|
52
|
+
{
|
53
|
+
at: @at,
|
54
|
+
total: @timing&.total,
|
55
|
+
real: @timing&.real,
|
56
|
+
}.compact
|
57
|
+
end
|
58
|
+
private :timing_to_h
|
59
|
+
|
60
|
+
def to_json(*args, &block)
|
61
|
+
to_h.to_json(*args, &block)
|
62
|
+
end
|
63
|
+
|
64
|
+
end # class Span
|
65
|
+
end # module Audit
|
66
|
+
end # module Startback
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Startback
|
2
|
+
module Audit
|
3
|
+
class TraceLogger
|
4
|
+
|
5
|
+
def initialize(logger = default_logger)
|
6
|
+
@logger = logger || default_logger
|
7
|
+
@logger.formatter ||= Support::LogFormatter.new if @logger.respond_to?(:formatter=)
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(span)
|
11
|
+
if !span.finished?
|
12
|
+
@logger.debug(span.to_h)
|
13
|
+
elsif span.success?
|
14
|
+
@logger.info(span.to_h)
|
15
|
+
elsif span&.error.is_a?(Startback::Errors::BadRequestError)
|
16
|
+
@logger.warn(span.to_h)
|
17
|
+
else
|
18
|
+
@logger.error(span.to_h)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def default_logger
|
25
|
+
::Logger.new(STDOUT, 'daily')
|
26
|
+
end
|
27
|
+
|
28
|
+
end # class TraceLogger
|
29
|
+
end # module Audit
|
30
|
+
end # module Startback
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
module Startback
|
5
|
+
module Audit
|
6
|
+
class Tracer
|
7
|
+
|
8
|
+
def initialize(stack = [], listeners = [], redactor = default_redactor)
|
9
|
+
@stack = stack
|
10
|
+
@listeners = listeners
|
11
|
+
@redactor = redactor
|
12
|
+
end
|
13
|
+
attr_reader :stack
|
14
|
+
|
15
|
+
def self.empty
|
16
|
+
Tracer.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def attached?
|
20
|
+
!@stack.empty?
|
21
|
+
end
|
22
|
+
|
23
|
+
def last_span!
|
24
|
+
error!("Trace not attached") unless attached?
|
25
|
+
|
26
|
+
@stack.last
|
27
|
+
end
|
28
|
+
|
29
|
+
def new_trace(attributes = {})
|
30
|
+
error!("Trace already attached") if attached?
|
31
|
+
|
32
|
+
attach_to(SecureRandom.uuid, SecureRandom.uuid, attributes)
|
33
|
+
end
|
34
|
+
|
35
|
+
def attach_to(trace_id, span_id, attributes = {}, parent_id = nil)
|
36
|
+
error!("Trace already attached") if attached?
|
37
|
+
|
38
|
+
initial_span = Span.new(trace_id, parent_id, attributes, span_id)
|
39
|
+
initial_stack = [ initial_span ]
|
40
|
+
Tracer.new(initial_stack, @listeners, @redactor)
|
41
|
+
end
|
42
|
+
|
43
|
+
def fork(attributes = {}, &block)
|
44
|
+
span = last_span!.fork(@redactor.redact(attributes))
|
45
|
+
@stack << span
|
46
|
+
propagate_to_listeners(span)
|
47
|
+
result, error = nil, nil
|
48
|
+
timing = Benchmark.measure do
|
49
|
+
result, error = exec_block_with_error_handling(block)
|
50
|
+
end
|
51
|
+
span = @stack.pop.finish(timing, error)
|
52
|
+
propagate_to_listeners(span)
|
53
|
+
error ? raise(error) : result
|
54
|
+
end
|
55
|
+
|
56
|
+
def on_span(listener = nil, &block)
|
57
|
+
@listeners << (listener || block)
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def default_redactor
|
64
|
+
Support::Redactor.new
|
65
|
+
end
|
66
|
+
|
67
|
+
def exec_block_with_error_handling(block)
|
68
|
+
[ block.call, nil ]
|
69
|
+
rescue => ex
|
70
|
+
[ nil, ex ]
|
71
|
+
end
|
72
|
+
|
73
|
+
def error!(msg)
|
74
|
+
raise Startback::Errors::InternalServerError, msg
|
75
|
+
end
|
76
|
+
|
77
|
+
def propagate_to_listeners(span)
|
78
|
+
@listeners.each do |listener|
|
79
|
+
listener.call(span)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end # class Tracer
|
84
|
+
end # module Audit
|
85
|
+
end # module Startback
|
data/lib/startback/audit.rb
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
require_relative 'audit/ext'
|
1
2
|
require_relative 'audit/shared'
|
2
|
-
require_relative 'audit/trailer'
|
3
3
|
require_relative 'audit/prometheus'
|
4
|
+
require_relative 'audit/span'
|
5
|
+
require_relative 'audit/null_tracer'
|
6
|
+
require_relative 'audit/tracer'
|
7
|
+
require_relative 'audit/operation_tracer'
|
8
|
+
require_relative 'audit/trace_logger'
|
9
|
+
require_relative 'audit/middleware'
|
@@ -2,6 +2,11 @@ module Startback
|
|
2
2
|
class Context
|
3
3
|
module HFactory
|
4
4
|
|
5
|
+
def inherited(subclass)
|
6
|
+
subclass.h_factories = h_factories.dup if h_factories?
|
7
|
+
subclass.h_dumpers = h_dumpers.dup if h_dumpers?
|
8
|
+
end
|
9
|
+
|
5
10
|
def h(hash)
|
6
11
|
h_factor!(self.new, hash)
|
7
12
|
end
|
@@ -13,6 +18,14 @@ module Startback
|
|
13
18
|
context
|
14
19
|
end
|
15
20
|
|
21
|
+
def h_factories?
|
22
|
+
!!@h_factories && @h_factories.any?
|
23
|
+
end
|
24
|
+
|
25
|
+
def h_factories=(factories)
|
26
|
+
@h_factories = factories
|
27
|
+
end
|
28
|
+
|
16
29
|
def h_factories
|
17
30
|
@h_factories ||= []
|
18
31
|
end
|
@@ -30,10 +43,18 @@ module Startback
|
|
30
43
|
hash
|
31
44
|
end
|
32
45
|
|
46
|
+
def h_dumpers?
|
47
|
+
!!@h_dumpers && @h_dumpers.any?
|
48
|
+
end
|
49
|
+
|
33
50
|
def h_dumpers
|
34
51
|
@h_dumpers ||= []
|
35
52
|
end
|
36
53
|
|
54
|
+
def h_dumpers=(dumpers)
|
55
|
+
@h_dumpers = dumpers
|
56
|
+
end
|
57
|
+
|
37
58
|
def h_dump(&dumper)
|
38
59
|
h_dumpers << dumper
|
39
60
|
end
|
data/lib/startback/context.rb
CHANGED
@@ -3,11 +3,10 @@ module Startback
|
|
3
3
|
|
4
4
|
def self.emits(type, &bl)
|
5
5
|
after_call do
|
6
|
-
event_data = instance_exec(&bl)
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
context.engine.bus.emit(event)
|
6
|
+
if event_data = instance_exec(&bl)
|
7
|
+
event = type.new(type.to_s, event_data, context)
|
8
|
+
context.engine.bus.emit(event)
|
9
|
+
end
|
11
10
|
end
|
12
11
|
end
|
13
12
|
|
@@ -3,16 +3,27 @@ module Startback
|
|
3
3
|
class FakeLogger < Logger
|
4
4
|
|
5
5
|
def initialize(*args)
|
6
|
-
@
|
6
|
+
@seen = []
|
7
|
+
end
|
8
|
+
attr_accessor :formatter
|
9
|
+
attr_reader :seen
|
10
|
+
|
11
|
+
def last_msg
|
12
|
+
seen.last
|
7
13
|
end
|
8
|
-
attr_reader :last_msg
|
9
14
|
|
10
15
|
[:debug, :info, :warn, :error, :fatal].each do |meth|
|
11
16
|
define_method(meth) do |msg|
|
12
|
-
@
|
13
|
-
end
|
17
|
+
@seen << format(meth, msg)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def format(severity, message)
|
22
|
+
return message unless formatter
|
23
|
+
|
24
|
+
formatter.call(severity.to_s.upcase, Time.now, 'prognam', message)
|
14
25
|
end
|
15
26
|
|
16
|
-
end # class
|
27
|
+
end # class FakeLogger
|
17
28
|
end # module Support
|
18
29
|
end # module Startback
|
@@ -2,16 +2,33 @@ module Startback
|
|
2
2
|
module Support
|
3
3
|
class LogFormatter
|
4
4
|
|
5
|
+
DEFAULT_OPTIONS = {
|
6
|
+
pretty_print: nil
|
7
|
+
}
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
11
|
+
@options[:pretty_print] ||= auto_pretty_print
|
12
|
+
end
|
13
|
+
|
14
|
+
def pretty_print?
|
15
|
+
!!@options[:pretty_print]
|
16
|
+
end
|
17
|
+
|
5
18
|
def call(severity, time, progname, msg)
|
6
19
|
msg = { message: msg } if msg.is_a?(String)
|
7
20
|
msg = { error: msg } if msg.is_a?(Exception)
|
8
|
-
{
|
21
|
+
data = {
|
9
22
|
severity: severity,
|
10
23
|
time: time
|
11
24
|
}.merge(msg)
|
12
25
|
.merge(error: error_to_json(msg[:error], severity))
|
13
26
|
.compact
|
14
|
-
|
27
|
+
if pretty_print?
|
28
|
+
JSON.pretty_generate(data) << "\n"
|
29
|
+
else
|
30
|
+
data.to_json << "\n"
|
31
|
+
end
|
15
32
|
end
|
16
33
|
|
17
34
|
def error_to_json(error, severity = nil)
|
@@ -29,6 +46,12 @@ module Startback
|
|
29
46
|
}.compact
|
30
47
|
end
|
31
48
|
|
49
|
+
private
|
50
|
+
|
51
|
+
def auto_pretty_print
|
52
|
+
ENV['RACK_ENV'] != 'production'
|
53
|
+
end
|
54
|
+
|
32
55
|
end # class LogFormatter
|
33
56
|
end # module Support
|
34
57
|
end # module Startback
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Startback
|
2
|
+
module Support
|
3
|
+
class Redactor
|
4
|
+
|
5
|
+
DEFAULT_OPTIONS = {
|
6
|
+
|
7
|
+
# Words used to stop dumping for, e.g., security reasons
|
8
|
+
blacklist: "token password secret credential email address"
|
9
|
+
|
10
|
+
}
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def redact(data)
|
17
|
+
case data
|
18
|
+
when Hash, OpenStruct
|
19
|
+
Hash[data.map{|(k,v)|
|
20
|
+
[k, (k.to_s =~ blacklist_rx) ? '---redacted---' : redact(v)]
|
21
|
+
}]
|
22
|
+
when Enumerable
|
23
|
+
data.map{|elm| redact(elm) }.compact
|
24
|
+
else
|
25
|
+
data
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def blacklist_rx
|
32
|
+
@blacklist_rx ||= Regexp.new(
|
33
|
+
@options[:blacklist].split(/\s+/).join("|"),
|
34
|
+
Regexp::IGNORECASE
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
end # class Redactor
|
39
|
+
end # module Support
|
40
|
+
end # module Startback
|
data/lib/startback/support.rb
CHANGED
data/lib/startback/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Startback
|
4
|
+
module Audit
|
5
|
+
describe "Context extension" do
|
6
|
+
subject do
|
7
|
+
context.to_h
|
8
|
+
end
|
9
|
+
|
10
|
+
context 'no tracing has been added' do
|
11
|
+
let(:context) do
|
12
|
+
Startback::Context.new
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'has no tracing info' do
|
16
|
+
expect(subject).not_to have_key('tracing')
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'does not break reloading a context if no tracing' do
|
20
|
+
subject = Startback::Context.h({})
|
21
|
+
expect(subject.tracer).to be_an_instance_of(Startback::Audit::Tracer)
|
22
|
+
expect(subject.tracer).not_to be_attached
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'some tracing has been added and attached' do
|
27
|
+
let(:context) do
|
28
|
+
Startback::Context.new.dup do |c|
|
29
|
+
c.tracer = Tracer.empty.attach_to('some_trace_uuid', 'some_trace_parent_uuid')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'has tracing info' do
|
34
|
+
expect(subject).to have_key('tracing')
|
35
|
+
expect(subject['tracing']['trace_id']).to eql('some_trace_uuid')
|
36
|
+
expect(subject['tracing']['span_id']).to eql('some_trace_parent_uuid')
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'helps reloading a tracer instance from h info' do
|
40
|
+
subject = Startback::Context.h('tracing' => {
|
41
|
+
'trace_id' => 'some_trace_uuid',
|
42
|
+
'parent_id' => 'some_trace_parent_uuid'
|
43
|
+
})
|
44
|
+
expect(subject.tracer).to be_an_instance_of(Startback::Audit::Tracer)
|
45
|
+
expect(subject.tracer).to be_attached
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'some tracing has been added and attached on a subclass' do
|
50
|
+
let(:context) do
|
51
|
+
SubContext.new.dup do |c|
|
52
|
+
c.tracer = Tracer.empty.attach_to('some_trace_uuid', 'some_trace_parent_uuid')
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'has tracing info' do
|
57
|
+
expect(context.tracer).to be_attached
|
58
|
+
expect(SubContext.h_factories.size).to eql(3)
|
59
|
+
expect(subject).to have_key('tracing')
|
60
|
+
expect(subject['tracing']['trace_id']).to eql('some_trace_uuid')
|
61
|
+
expect(subject['tracing']['span_id']).to eql('some_trace_parent_uuid')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end # module Audit
|
66
|
+
end # module Startback
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Startback
|
4
|
+
module Audit
|
5
|
+
describe Middleware do
|
6
|
+
include Rack::Test::Methods
|
7
|
+
|
8
|
+
def app
|
9
|
+
Rack::Builder.new do
|
10
|
+
use Startback::Context::Middleware
|
11
|
+
use Middleware
|
12
|
+
run ->(env) {
|
13
|
+
ctx = env[Startback::Context::Middleware::RACK_ENV_KEY]
|
14
|
+
attached = ctx.tracer.attached?
|
15
|
+
last_span = ctx.tracer.last_span!
|
16
|
+
[200, {
|
17
|
+
'tracer-attached' => attached,
|
18
|
+
'last-span' => last_span
|
19
|
+
}, 'ok']
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'when not provided with tracing headers' do
|
25
|
+
it 'starts a new trace' do
|
26
|
+
get '/'
|
27
|
+
expect(last_response.status).to eql(200)
|
28
|
+
expect(last_response.headers['tracer-attached']).to eq(true)
|
29
|
+
expect(last_response.body).to eql("ok")
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'creates a fresh new span' do
|
33
|
+
get '/'
|
34
|
+
expect(last_response.status).to eql(200)
|
35
|
+
expect(last_response.headers['last-span']).not_to be_nil
|
36
|
+
expect(last_response.body).to eql("ok")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'when provided with tracing headers' do
|
41
|
+
it 'does attach the tracer' do
|
42
|
+
header('X-Trace-Id', 'trace-id')
|
43
|
+
header('X-Span-Id', 'span-id')
|
44
|
+
get '/'
|
45
|
+
expect(last_response.status).to eql(200)
|
46
|
+
expect(last_response.headers['tracer-attached']).to eq(true)
|
47
|
+
expect(last_response.body).to eql("ok")
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'does create a new span' do
|
51
|
+
header('X-Trace-Id', 'trace-id')
|
52
|
+
header('X-Span-Id', 'span-id')
|
53
|
+
get '/'
|
54
|
+
expect(last_response.status).to eql(200)
|
55
|
+
|
56
|
+
span = last_response.headers['last-span']
|
57
|
+
expect(span).not_to be_nil
|
58
|
+
expect(span.trace_id).to eq('trace-id')
|
59
|
+
expect(span.parent_id).to eq('span-id')
|
60
|
+
expect(span.span_id).not_to be_nil
|
61
|
+
expect(span.span_id).not_to eq('span-id')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end # describe Middleware
|
65
|
+
end # module Audit
|
66
|
+
end # module Startback
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Startback
|
4
|
+
module Audit
|
5
|
+
describe TraceLogger do
|
6
|
+
|
7
|
+
let(:fake_logger) do
|
8
|
+
Support::FakeLogger.new
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:trace_logger) do
|
12
|
+
TraceLogger.new(fake_logger)
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:tracer) do
|
16
|
+
Tracer.new.on_span(trace_logger)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'helps logging successes' do
|
20
|
+
attached = tracer.attach_to('trace', 'root-span')
|
21
|
+
attached.fork(foo: 'bar') do
|
22
|
+
"hello world"
|
23
|
+
end
|
24
|
+
expect(fake_logger.seen.size).to eql(2)
|
25
|
+
expect(fake_logger.seen.first).to match(/DEBUG/)
|
26
|
+
expect(fake_logger.seen.last).to match(/INFO/)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'helps logging user errors' do
|
30
|
+
attached = tracer.attach_to('trace', 'root-span')
|
31
|
+
attached.fork(foo: 'bar') do
|
32
|
+
raise Startback::Errors::ForbiddenError, "no such access granted"
|
33
|
+
end rescue nil
|
34
|
+
expect(fake_logger.seen.size).to eql(2)
|
35
|
+
expect(fake_logger.seen.first).to match(/DEBUG/)
|
36
|
+
expect(fake_logger.seen.last).to match(/WARN/)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'helps logging fatal errors' do
|
40
|
+
attached = tracer.attach_to('trace', 'root-span')
|
41
|
+
attached.fork(foo: 'bar') do
|
42
|
+
raise ArgumentError, "something bad"
|
43
|
+
end rescue nil
|
44
|
+
expect(fake_logger.seen.size).to eql(2)
|
45
|
+
expect(fake_logger.seen.first).to match(/DEBUG/)
|
46
|
+
expect(fake_logger.seen.last).to match(/ERROR/)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Startback
|
4
|
+
module Audit
|
5
|
+
describe Tracer do
|
6
|
+
|
7
|
+
let(:spans_seen) do
|
8
|
+
[]
|
9
|
+
end
|
10
|
+
|
11
|
+
subject do
|
12
|
+
Tracer.new.on_span do |span|
|
13
|
+
spans_seen << span
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'clones the object on attach_to' do
|
18
|
+
expect(subject).not_to be_attached
|
19
|
+
attached = subject.attach_to('the-trace', 'root-span')
|
20
|
+
expect(attached).to be_a(Tracer)
|
21
|
+
expect(attached).not_to be(subject)
|
22
|
+
expect(attached).to be_attached
|
23
|
+
expect(subject).not_to be_attached
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'fork' do
|
27
|
+
it 'fails if not attached' do
|
28
|
+
expect(subject).not_to be_attached
|
29
|
+
expect {
|
30
|
+
subject.fork do
|
31
|
+
12
|
32
|
+
end
|
33
|
+
}.to raise_error(/attached/)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'returns the block result' do
|
37
|
+
attached = subject.attach_to('the-trace', 'root-span')
|
38
|
+
result = attached.fork do
|
39
|
+
'foo'
|
40
|
+
end
|
41
|
+
expect(result).to eql('foo')
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'propagates spans to listeners' do
|
45
|
+
attached = subject.attach_to('the-trace', 'root-span')
|
46
|
+
result = attached.fork do
|
47
|
+
'foo'
|
48
|
+
end
|
49
|
+
expect(spans_seen.size).to eql(2)
|
50
|
+
expect(spans_seen.last).to be_a(Span)
|
51
|
+
expect(spans_seen.last.timing).not_to be_nil
|
52
|
+
expect(spans_seen.last).to be_finished
|
53
|
+
expect(spans_seen.last).to be_success
|
54
|
+
expect(spans_seen.last).not_to be_error
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'reraises any error that occurs' do
|
58
|
+
attached = subject.attach_to('the-trace', 'root-span')
|
59
|
+
expect {
|
60
|
+
attached.fork do
|
61
|
+
raise ArgumentError, "An error"
|
62
|
+
end
|
63
|
+
}.to raise_error(ArgumentError)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'traces any error that occurs' do
|
67
|
+
attached = subject.attach_to('the-trace', 'root-span')
|
68
|
+
expect {
|
69
|
+
attached.fork do
|
70
|
+
raise ArgumentError, "An error"
|
71
|
+
end
|
72
|
+
}.to raise_error(ArgumentError)
|
73
|
+
expect(spans_seen.size).to eql(2)
|
74
|
+
expect(spans_seen.last).to be_a(Span)
|
75
|
+
expect(spans_seen.last.timing).not_to be_nil
|
76
|
+
expect(spans_seen.last).to be_finished
|
77
|
+
expect(spans_seen.last).not_to be_success
|
78
|
+
expect(spans_seen.last).to be_error
|
79
|
+
expect(spans_seen.last.error).to be_a(ArgumentError)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe 'redacting' do
|
84
|
+
it 'applies by default' do
|
85
|
+
attached = subject.attach_to('the-trace', 'root-span')
|
86
|
+
result = attached.fork({
|
87
|
+
foo: 'bar',
|
88
|
+
password: 'baz',
|
89
|
+
repeatPassword: 'baz'
|
90
|
+
}) do
|
91
|
+
'foo'
|
92
|
+
end
|
93
|
+
expect(spans_seen.size).to eql(2)
|
94
|
+
expect(spans_seen.last).to be_a(Span)
|
95
|
+
expect(spans_seen.last.attributes).to eql({
|
96
|
+
foo: 'bar',
|
97
|
+
password: '---redacted---',
|
98
|
+
repeatPassword: '---redacted---'
|
99
|
+
})
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
@@ -7,9 +7,11 @@ module Startback
|
|
7
7
|
expect(Context.new.to_json).to eql("{}")
|
8
8
|
end
|
9
9
|
|
10
|
-
it 'allows installing factories' do
|
11
|
-
expect(Context.h_factories).to
|
12
|
-
expect(SubContext.h_factories.size).to eql(
|
10
|
+
it 'allows installing factories and dumpers' do
|
11
|
+
expect(Context.h_factories.size).to eql(1)
|
12
|
+
expect(SubContext.h_factories.size).to eql(3)
|
13
|
+
expect(Context.h_dumpers.size).to eql(1)
|
14
|
+
expect(SubContext.h_dumpers.size).to eql(3)
|
13
15
|
end
|
14
16
|
|
15
17
|
it 'has a `to_h` information contract that works as expected' do
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Startback
|
4
|
+
module Support
|
5
|
+
describe Redactor do
|
6
|
+
|
7
|
+
let(:redactor) do
|
8
|
+
Redactor.new
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'applies default blacklists for security reasons' do
|
12
|
+
data = {
|
13
|
+
token: "will not be dumped",
|
14
|
+
a_token: "will not be dumped",
|
15
|
+
AToken: "will not be dumped",
|
16
|
+
password: "will not be dumped",
|
17
|
+
secret: "will not be dumped",
|
18
|
+
credentials: "will not be dumped",
|
19
|
+
foo: "bar"
|
20
|
+
}
|
21
|
+
expect(redactor.redact(data)).to eql({
|
22
|
+
token: "---redacted---",
|
23
|
+
a_token: "---redacted---",
|
24
|
+
AToken: "---redacted---",
|
25
|
+
password: "---redacted---",
|
26
|
+
secret: "---redacted---",
|
27
|
+
credentials: "---redacted---",
|
28
|
+
foo: "bar",
|
29
|
+
})
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'applies default blacklists to data arrays too' do
|
33
|
+
data = [{
|
34
|
+
token: "will not be dumped",
|
35
|
+
a_token: "will not be dumped",
|
36
|
+
AToken: "will not be dumped",
|
37
|
+
password: "will not be dumped",
|
38
|
+
secret: "will not be dumped",
|
39
|
+
credentials: "will not be dumped",
|
40
|
+
foo: "bar"
|
41
|
+
}]
|
42
|
+
expect(redactor.redact(data)).to eql([{
|
43
|
+
token: "---redacted---",
|
44
|
+
a_token: "---redacted---",
|
45
|
+
AToken: "---redacted---",
|
46
|
+
password: "---redacted---",
|
47
|
+
secret: "---redacted---",
|
48
|
+
credentials: "---redacted---",
|
49
|
+
foo: "bar"
|
50
|
+
}])
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'redacts recursively' do
|
54
|
+
data = [{
|
55
|
+
foo: "bar",
|
56
|
+
baz: {
|
57
|
+
password: 'will not be dumped'
|
58
|
+
}
|
59
|
+
}]
|
60
|
+
expect(redactor.redact(data)).to eql([{
|
61
|
+
foo: "bar",
|
62
|
+
baz: {
|
63
|
+
password: '---redacted---'
|
64
|
+
}
|
65
|
+
}])
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'uses the stop words provided at construction' do
|
69
|
+
r = Redactor.new(blacklist: "hello and world")
|
70
|
+
data = {
|
71
|
+
Hello: "bar",
|
72
|
+
World: "foo",
|
73
|
+
foo: "bar"
|
74
|
+
}
|
75
|
+
expect(r.redact(data)).to eql({
|
76
|
+
Hello: "---redacted---",
|
77
|
+
World: "---redacted---",
|
78
|
+
foo: "bar"
|
79
|
+
})
|
80
|
+
end
|
81
|
+
|
82
|
+
end # class Redactor
|
83
|
+
end # module Support
|
84
|
+
end # module Startback
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: startback
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.18.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bernard Lambeau
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-05-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -270,20 +270,20 @@ dependencies:
|
|
270
270
|
requirements:
|
271
271
|
- - ">="
|
272
272
|
- !ruby/object:Gem::Version
|
273
|
-
version: 0.
|
273
|
+
version: 0.21.0
|
274
274
|
- - "<"
|
275
275
|
- !ruby/object:Gem::Version
|
276
|
-
version: 0.
|
276
|
+
version: 0.22.0
|
277
277
|
type: :runtime
|
278
278
|
prerelease: false
|
279
279
|
version_requirements: !ruby/object:Gem::Requirement
|
280
280
|
requirements:
|
281
281
|
- - ">="
|
282
282
|
- !ruby/object:Gem::Version
|
283
|
-
version: 0.
|
283
|
+
version: 0.21.0
|
284
284
|
- - "<"
|
285
285
|
- !ruby/object:Gem::Version
|
286
|
-
version: 0.
|
286
|
+
version: 0.22.0
|
287
287
|
- !ruby/object:Gem::Dependency
|
288
288
|
name: tzinfo
|
289
289
|
requirement: !ruby/object:Gem::Requirement
|
@@ -382,9 +382,16 @@ files:
|
|
382
382
|
- Rakefile
|
383
383
|
- lib/startback.rb
|
384
384
|
- lib/startback/audit.rb
|
385
|
+
- lib/startback/audit/ext.rb
|
386
|
+
- lib/startback/audit/ext/context.rb
|
387
|
+
- lib/startback/audit/middleware.rb
|
388
|
+
- lib/startback/audit/null_tracer.rb
|
389
|
+
- lib/startback/audit/operation_tracer.rb
|
385
390
|
- lib/startback/audit/prometheus.rb
|
386
391
|
- lib/startback/audit/shared.rb
|
387
|
-
- lib/startback/audit/
|
392
|
+
- lib/startback/audit/span.rb
|
393
|
+
- lib/startback/audit/trace_logger.rb
|
394
|
+
- lib/startback/audit/tracer.rb
|
388
395
|
- lib/startback/caching/entity_cache.rb
|
389
396
|
- lib/startback/caching/no_store.rb
|
390
397
|
- lib/startback/caching/store.rb
|
@@ -419,6 +426,7 @@ files:
|
|
419
426
|
- lib/startback/support/log_formatter.rb
|
420
427
|
- lib/startback/support/logger.rb
|
421
428
|
- lib/startback/support/operation_runner.rb
|
429
|
+
- lib/startback/support/redactor.rb
|
422
430
|
- lib/startback/support/robustness.rb
|
423
431
|
- lib/startback/support/transaction_manager.rb
|
424
432
|
- lib/startback/support/transaction_policy.rb
|
@@ -436,8 +444,11 @@ files:
|
|
436
444
|
- lib/startback/web/prometheus.rb
|
437
445
|
- lib/startback/web/shield.rb
|
438
446
|
- spec/spec_helper.rb
|
447
|
+
- spec/unit/audit/ext/test_context.rb
|
448
|
+
- spec/unit/audit/test_middleware.rb
|
439
449
|
- spec/unit/audit/test_prometheus.rb
|
440
|
-
- spec/unit/audit/
|
450
|
+
- spec/unit/audit/test_trace_logger.rb
|
451
|
+
- spec/unit/audit/test_tracer.rb
|
441
452
|
- spec/unit/caching/test_entity_cache.rb
|
442
453
|
- spec/unit/context/test_abstraction_factory.rb
|
443
454
|
- spec/unit/context/test_dup.rb
|
@@ -454,6 +465,7 @@ files:
|
|
454
465
|
- spec/unit/support/operation_runner/test_before_after_call.rb
|
455
466
|
- spec/unit/support/test_data_object.rb
|
456
467
|
- spec/unit/support/test_env.rb
|
468
|
+
- spec/unit/support/test_redactor.rb
|
457
469
|
- spec/unit/support/test_robusteness.rb
|
458
470
|
- spec/unit/support/test_transaction_manager.rb
|
459
471
|
- spec/unit/support/test_world.rb
|
@@ -1,132 +0,0 @@
|
|
1
|
-
require_relative 'shared'
|
2
|
-
require 'forwardable'
|
3
|
-
module Startback
|
4
|
-
module Audit
|
5
|
-
#
|
6
|
-
# Log & Audit trail abstraction, that can be registered as an around
|
7
|
-
# hook on OperationRunner and as an actual logger on Context instances.
|
8
|
-
#
|
9
|
-
# The trail is outputted as JSON lines, using a Logger on the "device"
|
10
|
-
# passed at construction. The following JSON entries are dumped:
|
11
|
-
#
|
12
|
-
# - severity : INFO or ERROR
|
13
|
-
# - time : ISO8601 Datetime of operation execution
|
14
|
-
# - op : class name of the operation executed
|
15
|
-
# - op_took : Execution duration of the operation
|
16
|
-
# - op_data : Dump of operation input data
|
17
|
-
# - context : Execution context, through its `h` information contract (IC)
|
18
|
-
#
|
19
|
-
# Dumping of operation data follows the following duck typing conventions:
|
20
|
-
#
|
21
|
-
# - If the operation instance responds to `to_trail`, this data is taken
|
22
|
-
# - If the operation instance responds to `input`, this data is taken
|
23
|
-
# - If the operation instance responds to `request`, this data is taken
|
24
|
-
# - Otherwise op_data is a JSON null
|
25
|
-
#
|
26
|
-
# By contributing to the Context's `h` IC, users can easily dump information that
|
27
|
-
# makes sense (such as the operation execution requester).
|
28
|
-
#
|
29
|
-
# The class implements a sanitization process when dumping the context and
|
30
|
-
# operation data. Blacklisted words taken in construction options are used to
|
31
|
-
# prevent dumping hash keys that match them (insentively). Default stop words
|
32
|
-
# are equivalent to:
|
33
|
-
#
|
34
|
-
# Trailer.new("/var/log/trail.log", {
|
35
|
-
# blacklist: "token password secret credential"
|
36
|
-
# })
|
37
|
-
#
|
38
|
-
# Please note that the sanitization process does not apply recursively if
|
39
|
-
# the operation data is hierarchic. It only applies to the top object of
|
40
|
-
# Hash and [Hash]. Use `Operation#to_trail` to fine-tune your audit trail.
|
41
|
-
#
|
42
|
-
# Given that this Trailer is intended to be used as around hook on an
|
43
|
-
# `OperationRunner`, operations that fail at construction time will not be
|
44
|
-
# trailed at all, since they can't be ran in the first place. This may lead
|
45
|
-
# to trails not containing important errors cases if operations check their
|
46
|
-
# input at construction time.
|
47
|
-
#
|
48
|
-
class Trailer
|
49
|
-
include Shared
|
50
|
-
extend Forwardable
|
51
|
-
def_delegators :@logger, :debug, :info, :warn, :error, :fatal
|
52
|
-
|
53
|
-
DEFAULT_OPTIONS = {
|
54
|
-
|
55
|
-
# Words used to stop dumping for, e.g., security reasons
|
56
|
-
blacklist: "token password secret credential"
|
57
|
-
|
58
|
-
}
|
59
|
-
|
60
|
-
def initialize(device, options = {})
|
61
|
-
@options = DEFAULT_OPTIONS.merge(options)
|
62
|
-
@logger = ::Logger.new(device, 'daily')
|
63
|
-
@logger.formatter = Support::LogFormatter.new
|
64
|
-
end
|
65
|
-
attr_reader :logger, :options
|
66
|
-
|
67
|
-
def call(runner, op)
|
68
|
-
result = nil
|
69
|
-
time = Benchmark.realtime{ result = yield }
|
70
|
-
logger.info(op_to_trail(op, time))
|
71
|
-
result
|
72
|
-
rescue Startback::Errors::BadRequestError => ex
|
73
|
-
logger.warn(op_to_trail(op, time, ex))
|
74
|
-
raise
|
75
|
-
rescue => ex
|
76
|
-
logger.error(op_to_trail(op, time, ex))
|
77
|
-
raise
|
78
|
-
end
|
79
|
-
|
80
|
-
protected
|
81
|
-
|
82
|
-
def op_to_trail(op, time = nil, ex = nil)
|
83
|
-
log_msg = {
|
84
|
-
op_took: time ? time.round(8) : nil,
|
85
|
-
op: op_name(op),
|
86
|
-
context: op_context(op),
|
87
|
-
op_data: op_data(op)
|
88
|
-
}.compact
|
89
|
-
log_msg[:error] = ex if ex
|
90
|
-
log_msg
|
91
|
-
end
|
92
|
-
|
93
|
-
def op_context(op)
|
94
|
-
sanitize(op.respond_to?(:context, false) ? op.context.to_h : {})
|
95
|
-
end
|
96
|
-
|
97
|
-
def op_data(op)
|
98
|
-
data = if op.respond_to?(:op_data, false)
|
99
|
-
op.op_data
|
100
|
-
elsif op.respond_to?(:to_trail, false)
|
101
|
-
op.to_trail
|
102
|
-
elsif op.respond_to?(:input, false)
|
103
|
-
op.input
|
104
|
-
elsif op.respond_to?(:request, false)
|
105
|
-
op.request
|
106
|
-
elsif op.is_a?(Operation::MultiOperation)
|
107
|
-
op.ops.map{ |sub_op| op_to_trail(sub_op) }
|
108
|
-
end
|
109
|
-
sanitize(data)
|
110
|
-
end
|
111
|
-
|
112
|
-
def sanitize(data)
|
113
|
-
case data
|
114
|
-
when Hash, OpenStruct
|
115
|
-
data.dup.delete_if{|k| k.to_s =~ blacklist_rx }
|
116
|
-
when Enumerable
|
117
|
-
data.map{|elm| sanitize(elm) }.compact
|
118
|
-
else
|
119
|
-
data
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
def blacklist_rx
|
124
|
-
@blacklist_rx ||= Regexp.new(
|
125
|
-
options[:blacklist].split(/\s+/).join("|"),
|
126
|
-
Regexp::IGNORECASE
|
127
|
-
)
|
128
|
-
end
|
129
|
-
|
130
|
-
end # class Trailer
|
131
|
-
end # module Audit
|
132
|
-
end # module Startback
|
@@ -1,105 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require 'startback/audit'
|
3
|
-
module Startback
|
4
|
-
module Audit
|
5
|
-
describe Trailer do
|
6
|
-
|
7
|
-
let(:trailer) {
|
8
|
-
Trailer.new("/tmp/trail.log")
|
9
|
-
}
|
10
|
-
|
11
|
-
describe "op_name" do
|
12
|
-
|
13
|
-
def op_name(op, trailer = self.trailer)
|
14
|
-
trailer.send(:op_name, op)
|
15
|
-
end
|
16
|
-
|
17
|
-
it 'uses op_name in priority if provided' do
|
18
|
-
op = OpenStruct.new(op_name: "foo")
|
19
|
-
expect(op_name(op)).to eql("foo")
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
describe "op_data" do
|
24
|
-
|
25
|
-
def op_data(op, trailer = self.trailer)
|
26
|
-
trailer.send(:op_data, op)
|
27
|
-
end
|
28
|
-
|
29
|
-
it 'uses op_data in priority if provided' do
|
30
|
-
op = OpenStruct.new(op_data: { foo: "bar" }, input: 12, request: 13)
|
31
|
-
expect(op_data(op)).to eql({ foo: "bar" })
|
32
|
-
end
|
33
|
-
|
34
|
-
it 'uses to_trail then' do
|
35
|
-
op = OpenStruct.new(to_trail: { foo: "bar" }, input: 12, request: 13)
|
36
|
-
expect(op_data(op)).to eql({ foo: "bar" })
|
37
|
-
end
|
38
|
-
|
39
|
-
it 'uses input then' do
|
40
|
-
op = OpenStruct.new(input: { foo: "bar" }, request: 13)
|
41
|
-
expect(op_data(op)).to eql({ foo: "bar" })
|
42
|
-
end
|
43
|
-
|
44
|
-
it 'uses request then' do
|
45
|
-
op = OpenStruct.new(request: { foo: "bar" })
|
46
|
-
expect(op_data(op)).to eql({ foo: "bar" })
|
47
|
-
end
|
48
|
-
|
49
|
-
it 'applies default blacklists for security reasons' do
|
50
|
-
op = OpenStruct.new(input: {
|
51
|
-
token: "will not be dumped",
|
52
|
-
a_token: "will not be dumped",
|
53
|
-
AToken: "will not be dumped",
|
54
|
-
password: "will not be dumped",
|
55
|
-
secret: "will not be dumped",
|
56
|
-
credentials: "will not be dumped",
|
57
|
-
foo: "bar"
|
58
|
-
})
|
59
|
-
expect(op_data(op)).to eql({
|
60
|
-
foo: "bar"
|
61
|
-
})
|
62
|
-
end
|
63
|
-
|
64
|
-
it 'applies default blacklists to data arrays too' do
|
65
|
-
op = OpenStruct.new(input: [{
|
66
|
-
token: "will not be dumped",
|
67
|
-
a_token: "will not be dumped",
|
68
|
-
AToken: "will not be dumped",
|
69
|
-
password: "will not be dumped",
|
70
|
-
secret: "will not be dumped",
|
71
|
-
credentials: "will not be dumped",
|
72
|
-
foo: "bar"
|
73
|
-
}])
|
74
|
-
expect(op_data(op)).to eql([{
|
75
|
-
foo: "bar"
|
76
|
-
}])
|
77
|
-
end
|
78
|
-
|
79
|
-
it 'uses the stop words provided at construction' do
|
80
|
-
t = Trailer.new("/tmp/trail.log", blacklist: "hello and world")
|
81
|
-
op = OpenStruct.new(request: { Hello: "bar", World: "foo", foo: "bar" })
|
82
|
-
expect(op_data(op, t)).to eql({ foo: "bar" })
|
83
|
-
end
|
84
|
-
|
85
|
-
end
|
86
|
-
|
87
|
-
describe "op_context" do
|
88
|
-
|
89
|
-
def op_context(op, trailer = self.trailer)
|
90
|
-
trailer.send(:op_context, op)
|
91
|
-
end
|
92
|
-
|
93
|
-
it 'applies default blacklists for security reasons' do
|
94
|
-
op = OpenStruct.new(context: {
|
95
|
-
token: "will not be dumped",
|
96
|
-
foo: "bar"
|
97
|
-
})
|
98
|
-
expect(op_context(op)).to eql({ foo: "bar" })
|
99
|
-
end
|
100
|
-
|
101
|
-
end
|
102
|
-
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|