request-tracer 0.5.2
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 +15 -0
- data/.gemrelease +2 -0
- data/.gitignore +2 -0
- data/.travis.yml +22 -0
- data/Gemfile +8 -0
- data/Guardfile +24 -0
- data/LICENSE +13 -0
- data/README.md +35 -0
- data/Rakefile +16 -0
- data/lib/request_tracer/integration/base.rb +21 -0
- data/lib/request_tracer/integration/rack_handler.rb +18 -0
- data/lib/request_tracer/integration/rest_client_handler.rb +15 -0
- data/lib/request_tracer/trace.rb +159 -0
- data/lib/request_tracer/version.rb +3 -0
- data/lib/request_tracer.rb +10 -0
- data/request-tracer.gemspec +31 -0
- data/spec/integration/integration_spec.rb +91 -0
- data/spec/lib/request_tracer/integration/rack_handler_spec.rb +45 -0
- data/spec/lib/request_tracer/integration/rest_client_handler_spec.rb +23 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/support/test_app.rb +27 -0
- data/spec/support/test_app_config.ru +12 -0
- metadata +240 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZjEzN2Y3NGVjYTgzOGIwYzhlZDBiMjk4MDk1Y2M0ZjI2YThhZDU2MA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZWYzMDRiYjliZjk2MGJiOGM5Yzc4MTA2YzAxOGVmNWJlNDM0OWZiMA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YjJhZjU5MjRhYzg2MDY1MjNhMzkxNThkYTgyYTUyMDNmOGQ3MDU2NDQ2Y2U3
|
10
|
+
Y2ZjNjE5ZTQzNTYzMTVmNmE4ZDdkMmRjYjNjN2JmNDAyYWJlZTlkNzE4YmUy
|
11
|
+
NTRiYjc5ODY5ZjMxNzk3MDM3NjljZmEzNzE2MTI5MmRjYWU1MDA=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NDA1YjVhMmVhY2UwOWUxYTYwODE1Nzc4ZGFmNDZjNGMwZGU5MjEwOTZmYmE2
|
14
|
+
ZTQ5YTAzOTM5MmU0MWZjYTFhMjMyNDQyZDgwNGRhOTBjZmQ0NWNkMjlhMmM2
|
15
|
+
NTcxZTA0Y2FhMzM4NDljZWMyYzEwNGZhNjE1ZDJjMmMwYzQzZjI=
|
data/.gemrelease
ADDED
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
language: ruby
|
2
|
+
cache: bundler
|
3
|
+
rvm:
|
4
|
+
- 2.2.3
|
5
|
+
- 2.3.0
|
6
|
+
- jruby-head
|
7
|
+
- rbx
|
8
|
+
before_install:
|
9
|
+
- gem install bundler
|
10
|
+
matrix:
|
11
|
+
allow_failures:
|
12
|
+
- rvm: rbx
|
13
|
+
- rvm: jruby-head
|
14
|
+
sudo: false
|
15
|
+
deploy:
|
16
|
+
provider: rubygems
|
17
|
+
api_key:
|
18
|
+
secure: hUcjWlgTDJWwJyIx17z28TWjPOIoul9EvbcpBUFU5S87GKx6tysz+YPbsxVivF6pBKfvg3t3KpROKX7mZbO0/5TXXylY2nSLKa0SoVmbSbwIKtwkcYqEi8Hc77JRWHm5XOnqtYhC2UzzDUIOT/8MFacXq+D+06ZcFnB4jrndS66ZvTo6T81nWYS8rOBY+YDz5snwvbUNNAvXE/AdhnkSLBdPDE68AGEUzH9CT+x38UnN+ajXs+CqWDcvTSZVXSRhUqvH73SH5Ow+lApQfSZoxnvl70C9FShxIl18F9o1ax0q312si+p/1Q/x2Hq6C6PowncAwkP1qbDORp86uANUaspDfchXpsW6MSq0/a0jumN5gLTXB2pSRSxXWFFKv9wtOXAsO6+ig48NjqyczGsp02nZCOSHumw6xkoP4Bjg2zqk0OVd0dHsXDFa5r68nJsFeA7szYQI5/6GbTN3aqm5iCcTBGbVG/pQ9SEmvz9bcSeKoSGcIcTbb11QTj1rRPlsZ0L7Li0pN46/AwCniFMPD5nTTgKqHq1GfM6ot9IN7rVl7XbaQCHMedcpcrwjj2/hzKHhOdp4VjXm2Oc98dXxaQY3xziSKxpnMmYKHpg0nbUa4JNpYzuM9ZfnpMNri4WNdc2vE+p3TgNvNP2BFgpUQJGg1g5lTLe2w0Yo8MtNVFk=
|
19
|
+
gem: request-tracer
|
20
|
+
on:
|
21
|
+
tags: true
|
22
|
+
repo: crealytics/request-tracer
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
guard :bundler do
|
2
|
+
require 'guard/bundler'
|
3
|
+
require 'guard/bundler/verify'
|
4
|
+
helper = Guard::Bundler::Verify.new
|
5
|
+
|
6
|
+
files = ['Gemfile']
|
7
|
+
files += Dir['*.gemspec'] if files.any? { |f| helper.uses_gemspec?(f) }
|
8
|
+
|
9
|
+
# Assume files are symlinked from somewhere
|
10
|
+
files.each { |file| watch(helper.real_path(file)) }
|
11
|
+
end
|
12
|
+
|
13
|
+
guard :rspec, cmd: "bundle exec rspec -t focus" do
|
14
|
+
require "guard/rspec/dsl"
|
15
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
16
|
+
|
17
|
+
rspec = dsl.rspec
|
18
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
19
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
20
|
+
watch(rspec.spec_files)
|
21
|
+
|
22
|
+
ruby = dsl.ruby
|
23
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
24
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2016 crealytics GmbH
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
Request Tracer
|
2
|
+
==============
|
3
|
+
[](https://travis-ci.org/crealytics/request-tracer)
|
4
|
+
|
5
|
+
Request Tracer is a Ruby gem that helps tracing requests through a chain of services.
|
6
|
+
It is based on [ZipkinTracer](https://github.com/openzipkin/zipkin-tracer) but doesn't force you to use Zipkin.
|
7
|
+
|
8
|
+
One possible use case is to use your logger to log traces and spans and reuse your
|
9
|
+
existing log aggregation tool of choice (e.g. ELK) to get all logs across all services
|
10
|
+
that were involved in a client's service call.
|
11
|
+
|
12
|
+
How it works
|
13
|
+
------------
|
14
|
+
Request Tracer integrates with various other gems in order to transparently
|
15
|
+
read incoming trace headers and add trace headers to outgoing service calls.
|
16
|
+
A good introduction into Zipkin terminology [can be found here](http://www.slideshare.net/johanoskarsson/zipkin-strangeloop/25).
|
17
|
+
|
18
|
+
Reading trace headers
|
19
|
+
---------------------
|
20
|
+
In your `config.ru` add the RackHandler middleware like this:
|
21
|
+
```ruby
|
22
|
+
require 'request_tracer'
|
23
|
+
require 'request_tracer/integration/rack_handler'
|
24
|
+
|
25
|
+
use RequestTracer::Integration::RackHandler
|
26
|
+
run MyApp.new
|
27
|
+
```
|
28
|
+
|
29
|
+
Writing trace headers
|
30
|
+
---------------------
|
31
|
+
Somewhere in your code (e.g. in an initializer under `config/initializers/request-tracing.rb`)
|
32
|
+
add the following call:
|
33
|
+
```ruby
|
34
|
+
RequestTracer.integrate_with(:rest_client)
|
35
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'bundler'
|
4
|
+
require 'rake'
|
5
|
+
|
6
|
+
Bundler::GemHelper.install_tasks
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'rspec/core/rake_task'
|
10
|
+
|
11
|
+
RSpec::Core::RakeTask.new(:spec)
|
12
|
+
|
13
|
+
task :default => :spec
|
14
|
+
rescue LoadError
|
15
|
+
# no rspec available
|
16
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module RequestTracer
|
2
|
+
module Integration
|
3
|
+
module Base
|
4
|
+
B3_REQUIRED_FIELDS = %w(trace_id parent_span_id span_id)
|
5
|
+
B3_REQUIRED_FIELDS_FROM_SHORT_NAMES = B3_REQUIRED_FIELDS.map {|f| [f.gsub("_", ""), f] }.to_h
|
6
|
+
B3_REQUIRED_HEADERS = B3_REQUIRED_FIELDS.map {|f| "HTTP_X_B3_#{f.gsub("_", "").upcase}" }
|
7
|
+
B3_REQUIRED_FIELD_HEADER_MAP = B3_REQUIRED_FIELDS.zip(B3_REQUIRED_HEADERS).to_h
|
8
|
+
B3_REQUIRED_HEADER_FIELD_MAP = B3_REQUIRED_HEADERS.zip(B3_REQUIRED_FIELDS).to_h
|
9
|
+
B3_OPT_HEADERS = %w[HTTP_X_B3_FLAGS]
|
10
|
+
def extract_fields_from_headers(header_hash)
|
11
|
+
header_hash.map do |k,v|
|
12
|
+
special_header = /HTTP_X_B3_(.*)/.match(k)
|
13
|
+
special_header && [B3_REQUIRED_FIELDS_FROM_SHORT_NAMES[special_header[1].downcase], v]
|
14
|
+
end.compact.to_h
|
15
|
+
end
|
16
|
+
def extract_headers_from_fields(field_hash)
|
17
|
+
B3_REQUIRED_FIELDS.map {|f| ["X_B3_" + f.gsub("_", "").upcase, field_hash[f]]}.to_h
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module RequestTracer
|
4
|
+
module Integration
|
5
|
+
class RackHandler
|
6
|
+
include Base
|
7
|
+
def initialize(app, config={})
|
8
|
+
@app = app
|
9
|
+
@tracer = config[:tracer] || Trace
|
10
|
+
end
|
11
|
+
def call(env)
|
12
|
+
@tracer.record(extract_fields_from_headers(env)) do
|
13
|
+
@app.call(env)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
module RequestTracer
|
3
|
+
module Integration
|
4
|
+
module RestClientHandler
|
5
|
+
include Base
|
6
|
+
extend self
|
7
|
+
def activate
|
8
|
+
require 'rest-client'
|
9
|
+
RestClient.add_before_execution_proc do |req, params|
|
10
|
+
extract_headers_from_fields(Trace.latest).each {|h, v| req[h] = v}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module RequestTracer
|
2
|
+
module Trace
|
3
|
+
extend self
|
4
|
+
TRACE_ID_UPPER_BOUND = 2 ** 64
|
5
|
+
TRACE_STACK = :trace_stack
|
6
|
+
|
7
|
+
class Annotation
|
8
|
+
attr_reader :name, :time
|
9
|
+
def initialize(name, time = DateTime.now)
|
10
|
+
@name = name
|
11
|
+
@time = time
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# A span represents one specific method call
|
16
|
+
class SpanId
|
17
|
+
HEX_REGEX = /^[a-f0-9]{16}$/i
|
18
|
+
MAX_SIGNED_I64 = 9223372036854775807
|
19
|
+
MASK = (2 ** 64) - 1
|
20
|
+
|
21
|
+
def self.from_value(v)
|
22
|
+
if v.is_a?(String) && v =~ HEX_REGEX
|
23
|
+
new(v.hex)
|
24
|
+
elsif v.is_a?(Numeric)
|
25
|
+
new(v)
|
26
|
+
elsif v.is_a?(SpanId)
|
27
|
+
v
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(value)
|
32
|
+
@value = value
|
33
|
+
@i64 = if @value > MAX_SIGNED_I64
|
34
|
+
-1 * ((@value ^ MASK) + 1)
|
35
|
+
else
|
36
|
+
@value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s; "%016x" % @value; end
|
41
|
+
def to_i; @i64; end
|
42
|
+
end
|
43
|
+
|
44
|
+
# A trace is a set of spans that are associated with the same request
|
45
|
+
class TraceId
|
46
|
+
attr_reader :trace_id, :parent_id, :span_id
|
47
|
+
def self.spawn_from_hash(h)
|
48
|
+
span_id = Trace.generate_id
|
49
|
+
self.new(h["trace_id"] || span_id, h["span_id"], Trace.generate_id)
|
50
|
+
end
|
51
|
+
def initialize(trace_id, parent_id, span_id)
|
52
|
+
@trace_id = SpanId.from_value(trace_id)
|
53
|
+
@parent_id = parent_id && SpanId.from_value(parent_id)
|
54
|
+
@span_id = SpanId.from_value(span_id)
|
55
|
+
end
|
56
|
+
|
57
|
+
def next_id
|
58
|
+
TraceId.new(@trace_id, @span_id, Trace.generate_id)
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_s
|
62
|
+
"TraceId(trace_id = #{@trace_id.to_s}, parent_id = #{@parent_id.to_s}, span_id = #{@span_id.to_s}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_h
|
66
|
+
{"trace_id" => @trace_id.to_s, "parent_span_id" => (@parent_id || "").to_s, "span_id" => @span_id.to_s}
|
67
|
+
end
|
68
|
+
def [](key)
|
69
|
+
to_h[key]
|
70
|
+
end
|
71
|
+
def to_json
|
72
|
+
to_h.to_json
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def latest
|
78
|
+
if stack.empty?
|
79
|
+
span_id = generate_id
|
80
|
+
trace_id = TraceId.new(span_id, nil, span_id)
|
81
|
+
stack.push(trace_id)
|
82
|
+
end
|
83
|
+
stack.last
|
84
|
+
end
|
85
|
+
|
86
|
+
def push(trace_info)
|
87
|
+
stack.push(TraceId.spawn_from_hash(trace_info))
|
88
|
+
if block_given?
|
89
|
+
begin
|
90
|
+
yield
|
91
|
+
ensure
|
92
|
+
pop
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def pop
|
98
|
+
stack.pop
|
99
|
+
end
|
100
|
+
|
101
|
+
def unwind
|
102
|
+
if block_given?
|
103
|
+
begin
|
104
|
+
saved_stack = stack.dup
|
105
|
+
yield
|
106
|
+
ensure
|
107
|
+
stack = saved_stack
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def record(annotation, &block)
|
113
|
+
tracer.record(latest, annotation, &block)
|
114
|
+
end
|
115
|
+
|
116
|
+
def set_rpc_name(name)
|
117
|
+
tracer.set_rpc_name(latest, name) unless stack.empty?
|
118
|
+
end
|
119
|
+
|
120
|
+
def tracer=(tracer)
|
121
|
+
@tracer = tracer
|
122
|
+
end
|
123
|
+
|
124
|
+
def generate_id
|
125
|
+
rand(TRACE_ID_UPPER_BOUND)
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
# "stack" acts as a thread local variable and cannot be shared between
|
132
|
+
# threads.
|
133
|
+
def stack=(stack)
|
134
|
+
Thread.current[TRACE_STACK] = stack
|
135
|
+
end
|
136
|
+
|
137
|
+
def stack
|
138
|
+
Thread.current[TRACE_STACK] ||= []
|
139
|
+
end
|
140
|
+
|
141
|
+
def tracer
|
142
|
+
@tracer ||= DefaultTracer.new
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
class NullTracer
|
147
|
+
def record(*args, &block)
|
148
|
+
block.call
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
class DefaultTracer
|
153
|
+
def record(*args, &block)
|
154
|
+
Trace.push(args[1].to_h) do
|
155
|
+
block.call
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module RequestTracer
|
2
|
+
def self.integrate_with(*services)
|
3
|
+
services.each do |service|
|
4
|
+
require_relative "request_tracer/integration/#{service}_handler"
|
5
|
+
class_name = service.to_s.split('_').collect(&:capitalize).join + 'Handler'
|
6
|
+
integration_module = RequestTracer::Integration.const_get(class_name)
|
7
|
+
integration_module.activate
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require File.expand_path('../lib/request_tracer/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = "request-tracer"
|
6
|
+
spec.version = RequestTracer::VERSION
|
7
|
+
spec.authors = ["Martin Mauch"]
|
8
|
+
spec.email = ["martin.mauch@crealytics.com"]
|
9
|
+
spec.summary = %q{Traces requests using the Zipkin HTTP headers}
|
10
|
+
spec.description = %q{This is a tracer that hooks into several components to allow tracing requests across services.}
|
11
|
+
spec.homepage = ""
|
12
|
+
spec.license = "MIT"
|
13
|
+
|
14
|
+
spec.files = `git ls-files -z`.split("\x0")
|
15
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
16
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
17
|
+
spec.require_paths = ["lib"]
|
18
|
+
|
19
|
+
spec.add_development_dependency 'rest-client'
|
20
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
21
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
22
|
+
spec.add_development_dependency "rspec", "~> 3.4"
|
23
|
+
spec.add_development_dependency "rack-test", "~> 0.6"
|
24
|
+
spec.add_development_dependency "webmock"
|
25
|
+
spec.add_development_dependency "guard", "~> 2.13"
|
26
|
+
spec.add_development_dependency "guard-rspec", "~> 4.6"
|
27
|
+
spec.add_development_dependency "guard-bundler", "~> 2.1"
|
28
|
+
spec.add_development_dependency 'geminabox'
|
29
|
+
spec.add_development_dependency 'gem-release'
|
30
|
+
spec.add_development_dependency 'pry'
|
31
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'json'
|
3
|
+
require 'support/test_app'
|
4
|
+
require 'open-uri'
|
5
|
+
require 'timeout'
|
6
|
+
require 'webmock'
|
7
|
+
|
8
|
+
# In this spec we are going to run two applications and check that they are creating traces
|
9
|
+
# And that the traces created by one application are sent to the other application
|
10
|
+
RSpec::Matchers.define_negated_matcher :a_value_different_from, :eq
|
11
|
+
|
12
|
+
describe 'integrations' do
|
13
|
+
before(:all) do
|
14
|
+
WebMock.allow_net_connect!
|
15
|
+
@port1 = 4444
|
16
|
+
@port2 = 4445
|
17
|
+
ru_location = File.expand_path('../support/test_app_config.ru', File.dirname(__FILE__))
|
18
|
+
@pipe1 = IO.popen(["rackup", ru_location, "-p", @port1.to_s, err: [:child, :out]])
|
19
|
+
@pipe2 = IO.popen(["rackup", ru_location, "-p", @port2.to_s, err: [:child, :out]])
|
20
|
+
sleep(2)
|
21
|
+
if RUBY_PLATFORM == 'java'
|
22
|
+
sleep(20) #Jruby starts slow
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:base_url1) { "http://localhost:#{@port1}" }
|
27
|
+
let(:base_url2) { "http://localhost:#{@port2}" }
|
28
|
+
|
29
|
+
after(:all) do
|
30
|
+
Process.kill("KILL", @pipe1.pid)
|
31
|
+
Process.kill("KILL", @pipe2.pid)
|
32
|
+
end
|
33
|
+
|
34
|
+
after { read_all_pipes }
|
35
|
+
|
36
|
+
def read_without_blocking(io, maxlen = 4096)
|
37
|
+
io.read_nonblock(maxlen)
|
38
|
+
rescue => e
|
39
|
+
""
|
40
|
+
end
|
41
|
+
let(:response) { open(url) {|f| f.read } rescue(raise "Could not read from #{url}.\nOutput from services:\n#{read_all_pipes}") }
|
42
|
+
def read_all_pipes
|
43
|
+
read_without_blocking(@pipe1) + "\n" + read_without_blocking(@pipe2)
|
44
|
+
end
|
45
|
+
let(:services_output) {
|
46
|
+
response
|
47
|
+
read_all_pipes
|
48
|
+
}
|
49
|
+
let(:traces) do
|
50
|
+
all_output = services_output
|
51
|
+
all_output.split("\n").grep(/^%%%/).sort.map {|t| JSON.parse(t.gsub(/^%%% \d+/, ""))}
|
52
|
+
end
|
53
|
+
shared_examples_for "responding to a REST call without tracing headers" do
|
54
|
+
it "does not modify the original response body" do
|
55
|
+
expect(response).to eq(expected_response)
|
56
|
+
end
|
57
|
+
it "has a trace with non-empty trace_id and span_id and an empty parent_span_id" do
|
58
|
+
expect(traces[0]).to include(
|
59
|
+
'trace_id' => a_string_matching(/.+/),
|
60
|
+
'span_id' => a_string_matching(/.+/),
|
61
|
+
# The parent_span_id should be empty, as a 0-level trace has no parent.
|
62
|
+
'parent_span_id' => ""
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
shared_examples_for "responding to a transitive REST call with tracing headers" do
|
67
|
+
it "has the same trace_id as the caller, the caller's span id as parent_span_id and a fresh span_id" do
|
68
|
+
expect(traces[1]).to include(
|
69
|
+
'trace_id' => traces[0]['trace_id'],
|
70
|
+
'parent_span_id' => traces[0]['span_id'],
|
71
|
+
'span_id' => a_value_different_from(traces[0]['span_id'])
|
72
|
+
)
|
73
|
+
end
|
74
|
+
it { expect([traces[1]['trace_id'], traces[1]['parent_span_id']]).not_to include(traces[1]['span_id']) }
|
75
|
+
end
|
76
|
+
context "when doing a simple service call" do
|
77
|
+
let(:url) { "#{base_url1}/hello_world" }
|
78
|
+
let(:expected_response) { 'Hello World' }
|
79
|
+
it { expect(traces.size).to eq(1) }
|
80
|
+
it_behaves_like "responding to a REST call without tracing headers"
|
81
|
+
end
|
82
|
+
|
83
|
+
context "when doing a service call that calls another service" do
|
84
|
+
let(:url) { "#{base_url1}/ouroboros?out_port=#{@port2}" }
|
85
|
+
let(:expected_response) { 'Ouroboros says Hello World' }
|
86
|
+
it_behaves_like "responding to a REST call without tracing headers"
|
87
|
+
it_behaves_like "responding to a transitive REST call with tracing headers"
|
88
|
+
it { expect(traces).to satisfy {|t| t.size == 2} }
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rack/mock'
|
2
|
+
require 'rack/test'
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'request_tracer/integration/rack_handler'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
describe RequestTracer::Integration::RackHandler do
|
8
|
+
include Rack::Test::Methods
|
9
|
+
include RequestTracer::Integration::Base
|
10
|
+
let(:tracer) { double("tracer") }
|
11
|
+
|
12
|
+
def middleware(service, config={})
|
13
|
+
RequestTracer::Integration::RackHandler.new(service, config.merge(tracer: tracer))
|
14
|
+
end
|
15
|
+
|
16
|
+
def app
|
17
|
+
middleware(service)
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:service) do
|
21
|
+
lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['hello']] }
|
22
|
+
end
|
23
|
+
|
24
|
+
before do
|
25
|
+
allow(tracer).to receive(:record) {|&block| block.call }
|
26
|
+
end
|
27
|
+
|
28
|
+
shared_examples_for 'traces the request' do
|
29
|
+
before do
|
30
|
+
trace_headers.each {|k, v| header(k, v)}
|
31
|
+
end
|
32
|
+
it 'traces the request' do
|
33
|
+
get '/'
|
34
|
+
expect(last_response.status).to eq(200)
|
35
|
+
expect(last_response.body).to eq('hello')
|
36
|
+
expect(tracer).to have_received(:record).with(extract_fields_from_headers(trace_headers))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
let(:trace_headers) { RequestTracer::Integration::RackHandler::B3_REQUIRED_HEADERS.map {|a| [a, rand(1000)] }.to_h }
|
41
|
+
context 'Zipkin headers are passed to the middleware' do
|
42
|
+
subject { middleware(service) }
|
43
|
+
it_behaves_like "traces the request"
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rack/mock'
|
2
|
+
require 'rack/test'
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'request_tracer/trace'
|
5
|
+
require 'logger'
|
6
|
+
require 'webmock/rspec'
|
7
|
+
require 'rest-client'
|
8
|
+
|
9
|
+
describe RequestTracer::Integration::RestClientHandler do
|
10
|
+
include RequestTracer::Integration::Base
|
11
|
+
RequestTracer.integrate_with(:rest_client)
|
12
|
+
before(:all) { WebMock.disable_net_connect! }
|
13
|
+
before do
|
14
|
+
stub_request(:any, "www.example.com")
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:trace) { RequestTracer::Trace.latest }
|
18
|
+
it "should have set the headers on the outgoing call" do
|
19
|
+
RestClient.get("www.example.com")
|
20
|
+
expect(WebMock).to have_requested(:get, "www.example.com").
|
21
|
+
with(headers: extract_headers_from_fields(trace.to_h))
|
22
|
+
end
|
23
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'faraday'
|
3
|
+
require 'request_tracer'
|
4
|
+
require 'request_tracer/trace'
|
5
|
+
require 'rest-client'
|
6
|
+
|
7
|
+
class TestApp
|
8
|
+
RequestTracer.integrate_with(:rest_client)
|
9
|
+
def call(env)
|
10
|
+
store_current_trace_info # store so tests can look at historical data
|
11
|
+
|
12
|
+
req = Rack::Request.new(env)
|
13
|
+
if req.path == '/hello_world'
|
14
|
+
[ 200, {'Content-Type' => 'application/json'}, ['Hello World'] ]
|
15
|
+
elsif req.path == '/ouroboros' # this path will cause the TestApp to call the helloworld path of the app in certain port
|
16
|
+
port = Rack::Utils.parse_query(env['QUERY_STRING'], "&")['out_port']
|
17
|
+
response = RestClient.get("http://localhost:#{port}/hello_world")
|
18
|
+
[ 200, {'Content-Type' => 'application/json'}, ["Ouroboros says #{response}"]]
|
19
|
+
else
|
20
|
+
[ 500, {'Content-Type' => "text/txt"}, ["Unrecognized path #{req.path}"]]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def store_current_trace_info
|
25
|
+
$stderr.puts "%%% #{(Time.now.to_f * 1000).to_i} #{RequestTracer::Trace.latest.to_json}"
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'request_tracer'
|
2
|
+
require 'request_tracer/integration/rack_handler'
|
3
|
+
require 'base64'
|
4
|
+
require File.join(`pwd`.chomp, 'spec', 'support', 'test_app')
|
5
|
+
|
6
|
+
request_tracer_config = {
|
7
|
+
service_name: 'your service name here'
|
8
|
+
}
|
9
|
+
puts "Starting service"
|
10
|
+
|
11
|
+
use RequestTracer::Integration::RackHandler, request_tracer_config
|
12
|
+
run TestApp.new
|
metadata
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: request-tracer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Martin Mauch
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-02-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rest-client
|
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: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.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'
|
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'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack-test
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.6'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.6'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: webmock
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: guard
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ~>
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.13'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ~>
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.13'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: guard-rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '4.6'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ~>
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '4.6'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: guard-bundler
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ~>
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '2.1'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ~>
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '2.1'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: geminabox
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ! '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ! '>='
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: gem-release
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ! '>='
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ! '>='
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: pry
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ! '>='
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ! '>='
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
description: This is a tracer that hooks into several components to allow tracing
|
182
|
+
requests across services.
|
183
|
+
email:
|
184
|
+
- martin.mauch@crealytics.com
|
185
|
+
executables: []
|
186
|
+
extensions: []
|
187
|
+
extra_rdoc_files: []
|
188
|
+
files:
|
189
|
+
- .gemrelease
|
190
|
+
- .gitignore
|
191
|
+
- .travis.yml
|
192
|
+
- Gemfile
|
193
|
+
- Guardfile
|
194
|
+
- LICENSE
|
195
|
+
- README.md
|
196
|
+
- Rakefile
|
197
|
+
- lib/request_tracer.rb
|
198
|
+
- lib/request_tracer/integration/base.rb
|
199
|
+
- lib/request_tracer/integration/rack_handler.rb
|
200
|
+
- lib/request_tracer/integration/rest_client_handler.rb
|
201
|
+
- lib/request_tracer/trace.rb
|
202
|
+
- lib/request_tracer/version.rb
|
203
|
+
- request-tracer.gemspec
|
204
|
+
- spec/integration/integration_spec.rb
|
205
|
+
- spec/lib/request_tracer/integration/rack_handler_spec.rb
|
206
|
+
- spec/lib/request_tracer/integration/rest_client_handler_spec.rb
|
207
|
+
- spec/spec_helper.rb
|
208
|
+
- spec/support/test_app.rb
|
209
|
+
- spec/support/test_app_config.ru
|
210
|
+
homepage: ''
|
211
|
+
licenses:
|
212
|
+
- MIT
|
213
|
+
metadata: {}
|
214
|
+
post_install_message:
|
215
|
+
rdoc_options: []
|
216
|
+
require_paths:
|
217
|
+
- lib
|
218
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - ! '>='
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '0'
|
223
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
224
|
+
requirements:
|
225
|
+
- - ! '>='
|
226
|
+
- !ruby/object:Gem::Version
|
227
|
+
version: '0'
|
228
|
+
requirements: []
|
229
|
+
rubyforge_project:
|
230
|
+
rubygems_version: 2.4.5
|
231
|
+
signing_key:
|
232
|
+
specification_version: 4
|
233
|
+
summary: Traces requests using the Zipkin HTTP headers
|
234
|
+
test_files:
|
235
|
+
- spec/integration/integration_spec.rb
|
236
|
+
- spec/lib/request_tracer/integration/rack_handler_spec.rb
|
237
|
+
- spec/lib/request_tracer/integration/rest_client_handler_spec.rb
|
238
|
+
- spec/spec_helper.rb
|
239
|
+
- spec/support/test_app.rb
|
240
|
+
- spec/support/test_app_config.ru
|