roundtrip 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/Guardfile +6 -0
- data/LICENSE.txt +22 -0
- data/Procfile +0 -0
- data/README.md +115 -0
- data/Rakefile +1 -0
- data/benchmark/redis_store +47 -0
- data/bin/roundtrip +4 -0
- data/conf/roundtrip.yml +5 -0
- data/config.ru +9 -0
- data/examples/Gemfile +2 -0
- data/examples/roundtrip-schema.png +0 -0
- data/examples/ruby_httparty.rb +18 -0
- data/lib/roundtrip.rb +12 -0
- data/lib/roundtrip/cli.rb +23 -0
- data/lib/roundtrip/core.rb +61 -0
- data/lib/roundtrip/metrics.rb +5 -0
- data/lib/roundtrip/metrics/statsd.rb +13 -0
- data/lib/roundtrip/store.rb +5 -0
- data/lib/roundtrip/store/redis.rb +68 -0
- data/lib/roundtrip/trip.rb +37 -0
- data/lib/roundtrip/version.rb +3 -0
- data/lib/roundtrip/web.rb +122 -0
- data/roundtrip.gemspec +33 -0
- data/spec/roundtrip/core_spec.rb +122 -0
- data/spec/roundtrip/metrics/statsd_spec.rb +39 -0
- data/spec/roundtrip/store/redis_spec.rb +89 -0
- data/spec/roundtrip/trip_spec.rb +22 -0
- data/spec/spec_helper.rb +11 -0
- metadata +257 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Dotan Nahum
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Procfile
ADDED
File without changes
|
data/README.md
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
# Roundtrip
|
2
|
+
|
3
|
+
Roundtrip is a business process tracking and measurement service
|
4
|
+
especially useful for tracking distributed systems and services.
|
5
|
+
|
6
|
+
|
7
|
+
For a simple and useful explanation, check out [this blog
|
8
|
+
post](http://blog.paracode.com/2012/12/02/tracking-your-business/)
|
9
|
+
before you start.
|
10
|
+
|
11
|
+
|
12
|
+
<img src="https://github.com/jondot/roundtrip/raw/master/examples/roundtrip-schema.png" />
|
13
|
+
|
14
|
+
With it, you can answer these questions, in real-time:
|
15
|
+
|
16
|
+
* How is each part of my processing pipeline performing? How is the
|
17
|
+
entire pipeline performing?
|
18
|
+
* What out of my running transactions didn't end? isn't ending within X
|
19
|
+
amount of time?
|
20
|
+
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
You have a couple of options of running Roundtrip.
|
25
|
+
|
26
|
+
### Clone and run
|
27
|
+
|
28
|
+
Roundtrip currently supports HTTP as its RPC mechanism. This means you
|
29
|
+
can host it using your favorite battle-tested Ruby stack -- anything that can
|
30
|
+
run a Rack application.
|
31
|
+
|
32
|
+
$ git clone git://github.com/jondot/roundtrip.git
|
33
|
+
$ cd roundtrip
|
34
|
+
$ bundle
|
35
|
+
|
36
|
+
Now feel free to edit `config.ru` for Redis and Statsd configuration.
|
37
|
+
|
38
|
+
$ bundle exec rackup
|
39
|
+
|
40
|
+
### Install gem
|
41
|
+
|
42
|
+
You can install the `roundtrip` gem, and then use the web interface
|
43
|
+
within your existing Rack applications (for example, in Rails, you can
|
44
|
+
mount it).
|
45
|
+
|
46
|
+
$ gem install roundtrip
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
require 'roundtrip'
|
50
|
+
# set up the default Redis and Statsd components
|
51
|
+
Roundtrip.options[:redis] = { :host => 'localhost' }
|
52
|
+
Roundtrip.options[:statsd] = { :host => 'localhost', :port => 8125 }
|
53
|
+
|
54
|
+
require 'roundtrip/web'
|
55
|
+
# now mount/run Roundtrip::Web
|
56
|
+
```
|
57
|
+
|
58
|
+
|
59
|
+
|
60
|
+
## API Usage
|
61
|
+
|
62
|
+
I'll use `curl` just for illustration purposes. You should use what ever
|
63
|
+
HTTP library you feel comfertable with, within your code.
|
64
|
+
|
65
|
+
For usage examples on various platforms check out `/examples`.
|
66
|
+
|
67
|
+
|
68
|
+
Supply your own ID
|
69
|
+
|
70
|
+
```
|
71
|
+
curl -XPOST -d id=id-xyz&route=invoicing http://localhost:9292/trips
|
72
|
+
{"id":"id-xyz","route":"invoicing","started_at":"2012-11-30T18:23:23.814014+02:00"}
|
73
|
+
```
|
74
|
+
|
75
|
+
Or let roundtrip generate one for you
|
76
|
+
|
77
|
+
```
|
78
|
+
curl -XPOST -d route=invoicing http://localhost:9292/trips
|
79
|
+
{"id":"cf1999e8bfbd37963b1f92c527a8748e","route":"invoicing","started_at":"2012-11-30T18:23:23.814014+02:00"}
|
80
|
+
```
|
81
|
+
|
82
|
+
|
83
|
+
Using the generated ID, lets add checkpoints:
|
84
|
+
|
85
|
+
```
|
86
|
+
curl -XPATCH -dcheckpoint=generated.pdf http://localhost:9292/trips/cf1999e8bfbd37963b1f92c527a8748e
|
87
|
+
{"ok":true}
|
88
|
+
```
|
89
|
+
|
90
|
+
```
|
91
|
+
curl -XPATCH -dcheckpoint=emailed.customer http://localhost:9292/trips/cf1999e8bfbd37963b1f92c527a8748e
|
92
|
+
{"ok":true}
|
93
|
+
```
|
94
|
+
|
95
|
+
Let's finish this off, don't forget to do something with the JSON you
|
96
|
+
get back.
|
97
|
+
|
98
|
+
```
|
99
|
+
curl -XDELETE http://localhost:9292/trips/cf1999e8bfbd37963b1f92c527a8748e
|
100
|
+
{"id":"cf1999e8bfbd37963b1f92c527a8748e","route":"invoicing","started_at":"2012-11-30T18:54:20.098477+02:00","checkpoints":[["generated.pdf","2012-11-30T19:08:26.138140+02:00"],
|
101
|
+
["emailed.customer","2012-11-30T19:12:41.332270+02:00"]]}
|
102
|
+
```
|
103
|
+
|
104
|
+
|
105
|
+
# Contributing
|
106
|
+
|
107
|
+
Fork, implement, add tests, pull request, get my everlasting thanks and a respectable place here :).
|
108
|
+
|
109
|
+
|
110
|
+
# Copyright
|
111
|
+
|
112
|
+
|
113
|
+
Copyright (c) 2012 [Dotan Nahum](http://gplus.to/dotan) [@jondot](http://twitter.com/jondot). See MIT-LICENSE for further details.
|
114
|
+
|
115
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << File.expand_path('../lib', File.dirname(__FILE__))
|
4
|
+
require 'benchmark'
|
5
|
+
require 'roundtrip'
|
6
|
+
require 'roundtrip/core'
|
7
|
+
require 'roundtrip/store/redis'
|
8
|
+
require 'roundtrip/metrics/statsd'
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
# user system total real
|
14
|
+
# 0.270000 0.010000 0.280000 ( 0.268505) ; baseline
|
15
|
+
# 0.650000 0.540000 1.190000 ( 1.419452) ; start/end
|
16
|
+
# 2.050000 1.440000 3.490000 ( 4.284264) ; start/3xcheckpoint/end
|
17
|
+
|
18
|
+
# after adding metrics and statsd
|
19
|
+
# user system total real
|
20
|
+
# 0.260000 0.000000 0.260000 ( 0.263847)
|
21
|
+
# 0.770000 0.510000 1.280000 ( 1.476628)
|
22
|
+
# 2.080000 1.580000 3.660000 ( 4.488898)
|
23
|
+
#
|
24
|
+
|
25
|
+
core = Roundtrip::Core.new(Roundtrip::Store::Redis.new, Roundtrip::Metrics::Statsd.new)
|
26
|
+
Benchmark.bm do |x|
|
27
|
+
x.report do
|
28
|
+
10000.times{Roundtrip::Core.new(Roundtrip::Store::Redis.new, Roundtrip::Metrics::Statsd.new)}
|
29
|
+
end
|
30
|
+
|
31
|
+
x.report do
|
32
|
+
1000.times{ item = core.start('benchmark'); core.end(item.id) }
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
x.report do
|
37
|
+
1000.times do
|
38
|
+
item = core.start('benchmark')
|
39
|
+
core.checkpoint(item.id, 'foo1')
|
40
|
+
core.checkpoint(item.id, 'foo2')
|
41
|
+
core.checkpoint(item.id, 'foo3')
|
42
|
+
core.end(item.id)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
data/bin/roundtrip
ADDED
data/config.ru
ADDED
data/examples/Gemfile
ADDED
Binary file
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
|
3
|
+
#
|
4
|
+
# run Roundtrip on localhost:9292 (default rack).
|
5
|
+
#
|
6
|
+
|
7
|
+
resp = HTTParty.post 'http://localhost:9292/trips', :body => { :route => 'invoicing' }
|
8
|
+
|
9
|
+
# carry this ID around with you.
|
10
|
+
id = resp["id"]
|
11
|
+
|
12
|
+
HTTParty.patch "http://localhost:9292/trips/#{id}", :body => { :checkpoint => 'generate.pdf' }
|
13
|
+
HTTParty.patch "http://localhost:9292/trips/#{id}", :body => { :checkpoint => 'create.invoice' }
|
14
|
+
|
15
|
+
resp = HTTParty.delete "http://localhost:9292/trips/#{id}"
|
16
|
+
|
17
|
+
puts resp
|
18
|
+
|
data/lib/roundtrip.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'roundtrip'
|
2
|
+
require 'thor'
|
3
|
+
require 'roundtrip/store/redis'
|
4
|
+
|
5
|
+
class Roundtrip::CLI < Thor
|
6
|
+
|
7
|
+
desc "start [ROUTE]", "starts a new trip on a given route"
|
8
|
+
def start(route)
|
9
|
+
t = core.start(route)
|
10
|
+
say t.to_h
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "end [TRIP_ID]", "ends a trip"
|
14
|
+
def end(trip_id)
|
15
|
+
t = core.end(trip_id)
|
16
|
+
say t.to_h
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
def core
|
21
|
+
@core = Roundtrip::Core.new(Roundtrip::Store::Redis.new)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'roundtrip/trip'
|
2
|
+
|
3
|
+
class Roundtrip::Core
|
4
|
+
def initialize(store, metrics=Roundtrip::Metrics::Null.new)
|
5
|
+
@store = store
|
6
|
+
@metrics = metrics
|
7
|
+
end
|
8
|
+
|
9
|
+
def start(route, opts={})
|
10
|
+
must_be_present! route
|
11
|
+
|
12
|
+
t = Roundtrip::Trip.generate(route, opts)
|
13
|
+
@store.add(t)
|
14
|
+
t
|
15
|
+
end
|
16
|
+
|
17
|
+
def checkpoint(id, at)
|
18
|
+
must_be_present! id, at
|
19
|
+
trip = @store.get(id)
|
20
|
+
if trip
|
21
|
+
res = @store.add_checkpoint(trip, at)
|
22
|
+
@metrics.time(trip.route, at, msec(res[1] - trip.started_at))
|
23
|
+
res
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def end(id)
|
28
|
+
must_be_present! id
|
29
|
+
|
30
|
+
trip = @store.get(id)
|
31
|
+
end_trip(trip) if trip
|
32
|
+
trip
|
33
|
+
end
|
34
|
+
|
35
|
+
def pending(route, older_than=0)
|
36
|
+
must_be_present! route
|
37
|
+
|
38
|
+
@store.pending_trips(route, older_than)
|
39
|
+
end
|
40
|
+
|
41
|
+
def purge(route, older_than=0)
|
42
|
+
pending_trips = pending(route, older_than)
|
43
|
+
pending_trips.each{ |trip| end_trip(trip) }
|
44
|
+
pending_trips
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def end_trip(trip)
|
49
|
+
@store.remove(trip)
|
50
|
+
@metrics.time(trip.route, 'end', msec(Time.now - trip.started_at))
|
51
|
+
end
|
52
|
+
|
53
|
+
def msec(floating_delta)
|
54
|
+
(floating_delta*1000).floor
|
55
|
+
end
|
56
|
+
def must_be_present!(*things)
|
57
|
+
things.each do |thing|
|
58
|
+
raise "parameter must be present" unless thing && thing.strip != ""
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'statsd'
|
2
|
+
|
3
|
+
|
4
|
+
class Roundtrip::Metrics::Statsd
|
5
|
+
def initialize(opts={:statsd => {:host => 'localhost', :port => 8125}})
|
6
|
+
@statsd =opts[:statsd][:connection] || ::Statsd.new(opts[:statsd][:host], opts[:statsd][:port])
|
7
|
+
end
|
8
|
+
|
9
|
+
def time(route, event, time)
|
10
|
+
@statsd.timing("#{route}.#{event}", time)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'roundtrip/store'
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
|
5
|
+
class Roundtrip::Store::Redis
|
6
|
+
def initialize(opts={:redis => {:host => 'localhost'}})
|
7
|
+
@conn = opts[:redis][:connection] || Redis.new(opts[:redis])
|
8
|
+
end
|
9
|
+
|
10
|
+
def get(id)
|
11
|
+
blob = @conn.get(trip_key(id))
|
12
|
+
trip = Marshal.load(blob) if blob
|
13
|
+
if trip
|
14
|
+
trip.checkpoints = @conn.zrange(checkpoints_key(id), "0", "-1", :withscores => true).map{|pair| [pair[0], Time.at(pair[1])]}
|
15
|
+
end
|
16
|
+
trip
|
17
|
+
end
|
18
|
+
|
19
|
+
def add(trip)
|
20
|
+
@conn.multi do
|
21
|
+
@conn.set(trip_key(trip.id), Marshal.dump(trip))
|
22
|
+
@conn.zadd(route_key(trip.route), trip.started_at.to_i, trip.id)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_checkpoint(trip, at)
|
27
|
+
time = Time.now
|
28
|
+
@conn.zadd(checkpoints_key(trip.id), time.to_f, at)
|
29
|
+
[at, time]
|
30
|
+
end
|
31
|
+
|
32
|
+
def remove(trip)
|
33
|
+
res = @conn.multi do
|
34
|
+
@conn.del(trip_key(trip.id))
|
35
|
+
@conn.del(checkpoints_key(trip.id))
|
36
|
+
@conn.zrem(route_key(trip.route), trip.id)
|
37
|
+
end
|
38
|
+
res.count == 3 && res[0] == 1 && res[2] == true
|
39
|
+
end
|
40
|
+
|
41
|
+
def pending_trips(prod, older_than=0)
|
42
|
+
start_stamp = Time.now.to_i - older_than
|
43
|
+
ids = @conn.zrangebyscore(route_key(prod), 0, start_stamp.to_s)
|
44
|
+
return [] if ids.empty?
|
45
|
+
|
46
|
+
tripsdata = @conn.mget(ids.map{|k| trip_key(k) })
|
47
|
+
tripsdata.map { |blob| Marshal.load(blob) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def prune_old_trips(prod, timespan)
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
|
56
|
+
private
|
57
|
+
def trip_key(trip_id)
|
58
|
+
"rt:trip:#{trip_id}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def route_key(route)
|
62
|
+
"rt:route:#{route}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def checkpoints_key(trip_id)
|
66
|
+
"rt:cp:#{trip_id}"
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
|
5
|
+
class Roundtrip::Trip
|
6
|
+
attr_accessor :id, :route, :started_at, :checkpoints
|
7
|
+
|
8
|
+
def initialize(id, route, started_at)
|
9
|
+
self.id = id
|
10
|
+
self.route = route
|
11
|
+
self.started_at = started_at
|
12
|
+
self.checkpoints = []
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def to_json(*a)
|
17
|
+
to_h.to_json(*a)
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_h
|
21
|
+
{
|
22
|
+
:id => id,
|
23
|
+
:route => route,
|
24
|
+
:started_at => started_at.iso8601(6),
|
25
|
+
:checkpoints => checkpoints.map{|cp| [cp[0], cp[1].iso8601(6)] }
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def ==(o)
|
30
|
+
o.class == self.class && o.to_h == to_h
|
31
|
+
end
|
32
|
+
alias_method :eql?, :==
|
33
|
+
|
34
|
+
def self.generate(route, opts={})
|
35
|
+
new(opts[:id] || SecureRandom.hex, route, Time.now)
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'roundtrip'
|
2
|
+
require 'roundtrip/store/redis'
|
3
|
+
require 'roundtrip/metrics/statsd'
|
4
|
+
|
5
|
+
|
6
|
+
require 'sinatra'
|
7
|
+
require 'json'
|
8
|
+
require 'atom'
|
9
|
+
|
10
|
+
# XXX
|
11
|
+
# put on heroku
|
12
|
+
# change url to be provider based at admin
|
13
|
+
# set up configuration for redis to one of the test instances
|
14
|
+
# check cross-cycle
|
15
|
+
#
|
16
|
+
class Roundtrip::Web < Sinatra::Base
|
17
|
+
|
18
|
+
configure do
|
19
|
+
store = Roundtrip::Store::Redis.new(Roundtrip.options)
|
20
|
+
stats = Roundtrip::Metrics::Statsd.new(Roundtrip.options)
|
21
|
+
|
22
|
+
set :core, Roundtrip::Core.new(store, stats)
|
23
|
+
end
|
24
|
+
|
25
|
+
post '/trips' do
|
26
|
+
content_type :json
|
27
|
+
route = params[:route]
|
28
|
+
must_exist!(route)
|
29
|
+
|
30
|
+
id = params[:id]
|
31
|
+
|
32
|
+
trip = core.start(route, :id => id)
|
33
|
+
# XXX add location: header
|
34
|
+
json trip
|
35
|
+
end
|
36
|
+
|
37
|
+
patch '/trips/:id' do
|
38
|
+
content_type :json
|
39
|
+
id = params[:id]
|
40
|
+
checkpoint = params[:checkpoint]
|
41
|
+
must_exist! id
|
42
|
+
must_exist! checkpoint
|
43
|
+
|
44
|
+
core.checkpoint(id, checkpoint)
|
45
|
+
json(:ok => true)
|
46
|
+
end
|
47
|
+
|
48
|
+
delete '/trips/:id' do
|
49
|
+
content_type :json
|
50
|
+
id = params[:id]
|
51
|
+
must_exist!(id)
|
52
|
+
|
53
|
+
trip = core.end(id)
|
54
|
+
json trip
|
55
|
+
end
|
56
|
+
|
57
|
+
delete '/trips' do
|
58
|
+
# XXX nastily un-dry. will fix later
|
59
|
+
content_type :json
|
60
|
+
route = params[:route]
|
61
|
+
older_than = params[:older_than_secs]
|
62
|
+
|
63
|
+
must_exist!(route)
|
64
|
+
|
65
|
+
res = core.purge(route, (older_than || 0).to_i)
|
66
|
+
|
67
|
+
json res
|
68
|
+
end
|
69
|
+
|
70
|
+
get '/trips.json' do
|
71
|
+
content_type :json
|
72
|
+
route = params[:route]
|
73
|
+
older_than = params[:older_than_secs]
|
74
|
+
|
75
|
+
must_exist!(route)
|
76
|
+
|
77
|
+
res = core.pending(route, (older_than || 0).to_i)
|
78
|
+
json res
|
79
|
+
end
|
80
|
+
|
81
|
+
get '/trips.rss' do
|
82
|
+
content_type 'text/xml'
|
83
|
+
route = params[:route]
|
84
|
+
must_exist!(route)
|
85
|
+
|
86
|
+
older_than = params[:older_than_secs]
|
87
|
+
|
88
|
+
res = core.pending(route, (older_than || "0").to_i)
|
89
|
+
|
90
|
+
feed = Atom::Feed.new do |f|
|
91
|
+
f.title = "Roundtrip #{route}"
|
92
|
+
f.links << Atom::Link.new(:href => "http://example.com/roundtrip/#{route}/")
|
93
|
+
|
94
|
+
res.each do |p|
|
95
|
+
f.entries << Atom::Entry.new do |e|
|
96
|
+
e.title = p.id
|
97
|
+
e.links << Atom::Link.new(:href => "http://example.com/#{p.id}")
|
98
|
+
e.id = p.id
|
99
|
+
e.updated = p.started_at.iso8601(6)
|
100
|
+
e.summary = p.started_at.iso8601(6)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
feed.to_xml
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
def core
|
109
|
+
settings.core
|
110
|
+
end
|
111
|
+
|
112
|
+
def json(obj)
|
113
|
+
obj.to_json
|
114
|
+
end
|
115
|
+
|
116
|
+
def must_exist!(thing)
|
117
|
+
# XXX say which param. accept :param => val hash
|
118
|
+
halt 406, {:error => "Parameter must be present."}.to_json unless thing && thing.strip != ""
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
|
data/roundtrip.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'roundtrip/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "roundtrip"
|
8
|
+
gem.version = Roundtrip::VERSION
|
9
|
+
gem.authors = ["Dotan Nahum"]
|
10
|
+
gem.email = ["jondotan@gmail.com"]
|
11
|
+
gem.description = %q{Simple business process/transactions tracking and metrics service}
|
12
|
+
gem.summary = %q{Simple business process/transactions tracking and metrics service}
|
13
|
+
gem.homepage = "https://github.com/jondot/roundtrip"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency "thor"
|
21
|
+
gem.add_dependency "sinatra"
|
22
|
+
gem.add_dependency "redis-namespace"
|
23
|
+
gem.add_dependency "redis"
|
24
|
+
gem.add_dependency "json"
|
25
|
+
gem.add_dependency "ratom"
|
26
|
+
gem.add_dependency "thor"
|
27
|
+
gem.add_dependency "statsd-ruby"
|
28
|
+
|
29
|
+
gem.add_development_dependency "guard-minitest"
|
30
|
+
gem.add_development_dependency "rr"
|
31
|
+
gem.add_development_dependency "timecop"
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'roundtrip/core'
|
4
|
+
|
5
|
+
|
6
|
+
module Roundtrip
|
7
|
+
describe Core do
|
8
|
+
let(:trip_id) { 'C0FFEEBABE' }
|
9
|
+
let(:trip) do
|
10
|
+
Trip.new(trip_id, 'prod', Time.now)
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#start" do
|
14
|
+
it "should start" do
|
15
|
+
mock(Roundtrip::Trip).generate('prod', {}){ trip }
|
16
|
+
store = Object.new
|
17
|
+
mock(store).add(trip){true}
|
18
|
+
core = Core.new(store)
|
19
|
+
core.start('prod').must_equal(trip)
|
20
|
+
end
|
21
|
+
it "should start with an explicit ID given it was supplied" do
|
22
|
+
mock(Roundtrip::Trip).generate('prod', {:id => 'id-xyz'}){ trip }
|
23
|
+
store = Object.new
|
24
|
+
mock(store).add(trip){true}
|
25
|
+
core = Core.new(store)
|
26
|
+
core.start('prod', :id => 'id-xyz').must_equal(trip)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#checkpoint" do
|
31
|
+
it "should add a new checkpoint to a started trip" do
|
32
|
+
store = Object.new
|
33
|
+
mock(store).get("id-xyz"){ trip }
|
34
|
+
time = Time.now
|
35
|
+
mock(store).add_checkpoint(trip, 'generate_invoice'){ ['generate_invoice', time] }
|
36
|
+
|
37
|
+
core = Core.new(store)
|
38
|
+
t = core.checkpoint('id-xyz', 'generate_invoice')
|
39
|
+
t.must_equal ['generate_invoice', time]
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should not add a new checkpoint to a missing trip" do
|
43
|
+
store = Object.new
|
44
|
+
mock(store).get("id-xyz"){ nil }
|
45
|
+
|
46
|
+
core = Core.new(store)
|
47
|
+
core.checkpoint('id-xyz', 'generate_invoice').must_be_nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
describe "#end" do
|
51
|
+
it "should not end given it never started" do
|
52
|
+
store = Object.new
|
53
|
+
mock(store).get('bad-trip'){ nil }
|
54
|
+
core = Core.new(store)
|
55
|
+
core.end('bad-trip').must_be_nil
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
it "should end the exact ID that has been started" do
|
60
|
+
t = Time.now
|
61
|
+
trip = Trip.new('key-1', 'prod', t)
|
62
|
+
mock(Roundtrip::Trip).generate('key-1', {}){ trip }
|
63
|
+
store = Object.new
|
64
|
+
mock(store).add(trip){ true }
|
65
|
+
mock(store).get(trip.id){ trip }
|
66
|
+
mock(store).remove(trip){ true }
|
67
|
+
|
68
|
+
core = Core.new(store)
|
69
|
+
|
70
|
+
core.start('key-1')
|
71
|
+
core.end('key-1').must_equal(trip)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#pending" do
|
76
|
+
it "should list all open trips" do
|
77
|
+
store = Object.new
|
78
|
+
pending = [ :foo ]
|
79
|
+
mock(store).pending_trips('prod', 0){ pending }
|
80
|
+
|
81
|
+
core = Core.new(store)
|
82
|
+
|
83
|
+
core.pending('prod', 0).must_equal(pending)
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
describe "#purge" do
|
89
|
+
it "should do nothing given no pending trips" do
|
90
|
+
store = Object.new
|
91
|
+
pending = []
|
92
|
+
mock(store).pending_trips('prod', 0){ pending }
|
93
|
+
|
94
|
+
core = Core.new(store)
|
95
|
+
core.purge('prod', 0).must_equal(pending)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should purge pending trips given staleness" do
|
99
|
+
store = Object.new
|
100
|
+
trip1 = Trip.new('key-1', 'prod', Time.now)
|
101
|
+
pending = [trip1]
|
102
|
+
mock(store).pending_trips('prod', 30){ pending }
|
103
|
+
mock(store).remove(trip1)
|
104
|
+
|
105
|
+
core = Core.new(store)
|
106
|
+
core.purge('prod', 30).must_equal(pending)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should purge all pending trips given no staleness" do
|
110
|
+
store = Object.new
|
111
|
+
trip1 = Trip.new('key-1', 'prod', Time.now)
|
112
|
+
pending = [trip1]
|
113
|
+
mock(store).pending_trips('prod', 0){ pending }
|
114
|
+
mock(store).remove(trip1)
|
115
|
+
|
116
|
+
core = Core.new(store)
|
117
|
+
core.purge('prod').must_equal(pending)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'roundtrip/metrics/statsd'
|
3
|
+
require 'timecop'
|
4
|
+
|
5
|
+
module Roundtrip
|
6
|
+
describe Metrics::Statsd do
|
7
|
+
it "should time checkpoints" do
|
8
|
+
trip = Trip.new('trip', 'prod', Time.at(1354366786.0090559))
|
9
|
+
|
10
|
+
statsd = Object.new
|
11
|
+
mock(statsd).timing("prod.invoice.created", 2789)
|
12
|
+
metrics = Metrics::Statsd.new(:statsd => { :connection => statsd })
|
13
|
+
|
14
|
+
store = Object.new
|
15
|
+
mock(store).get('id-xyz'){ trip }
|
16
|
+
mock(store).add_checkpoint(trip, 'invoice.created'){ [trip, Time.at(1354366788.7987247)] }
|
17
|
+
|
18
|
+
core = Core.new(store, metrics)
|
19
|
+
core.checkpoint('id-xyz', 'invoice.created')
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should time end" do
|
23
|
+
trip = Trip.new('trip', 'prod', Time.at(1354366786.0090559))
|
24
|
+
|
25
|
+
statsd = Object.new
|
26
|
+
mock(statsd).timing("prod.end", 2789)
|
27
|
+
metrics = Metrics::Statsd.new(:statsd => { :connection => statsd })
|
28
|
+
|
29
|
+
Timecop.freeze(Time.at(1354366788.7987247)) do
|
30
|
+
store = Object.new
|
31
|
+
mock(store).get('id-xyz'){ trip }
|
32
|
+
mock(store).remove(trip)
|
33
|
+
|
34
|
+
core = Core.new(store, metrics)
|
35
|
+
core.end('id-xyz')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'roundtrip/store/redis'
|
3
|
+
require 'redis'
|
4
|
+
require 'redis/namespace'
|
5
|
+
require 'timecop'
|
6
|
+
|
7
|
+
|
8
|
+
# dirty test. requires running a local redis
|
9
|
+
# todo: check how redis rb does its tests. (and how integrates with travis-ci)
|
10
|
+
module Roundtrip
|
11
|
+
describe Store::Redis do
|
12
|
+
|
13
|
+
before do
|
14
|
+
@redis = ::Redis::Namespace.new('roundtrip-testing', :redis => Redis.new)
|
15
|
+
ks = @redis.keys "rt:*"
|
16
|
+
@redis.pipelined do
|
17
|
+
ks.each{ |k| @redis.del k }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:store){ Store::Redis.new({ :redis => { :connection => @redis }}) }
|
22
|
+
|
23
|
+
it "should add and get trips" do
|
24
|
+
t = Trip.new('trip', 'prod', Time.now)
|
25
|
+
store.add(t)
|
26
|
+
store.get(t.id).must_equal(t)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should get a trip by ID" do
|
30
|
+
t = Trip.new('trip', 'prod', Time.now)
|
31
|
+
id = store.add(t)
|
32
|
+
|
33
|
+
r = store.get('trip')
|
34
|
+
r.id.must_equal 'trip'
|
35
|
+
r.route.must_equal 'prod'
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should remove a trip by ID" do
|
39
|
+
t = Trip.new('trip', 'prod', Time.now)
|
40
|
+
id = store.add(t)
|
41
|
+
store.remove(t).must_equal(true)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should add a checkpoint" do
|
45
|
+
t = Trip.new('trip', 'id-xyz', Time.now)
|
46
|
+
store.add(t)
|
47
|
+
time = Time.now
|
48
|
+
Timecop.freeze(time) do
|
49
|
+
store.add_checkpoint(t, 'generate.invoice')
|
50
|
+
trip = store.get(t.id)
|
51
|
+
checkpoint, ts = trip.checkpoints[0]
|
52
|
+
checkpoint.must_equal 'generate.invoice'
|
53
|
+
ts.to_f.must_equal time.to_f
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
describe "#pending_trips" do
|
59
|
+
it "should fetch pending trips" do
|
60
|
+
t = Trip.new('trip', 'prod', Time.now)
|
61
|
+
id = store.add(t)
|
62
|
+
pending = store.pending_trips('prod')
|
63
|
+
pending.first.must_equal t
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should return empty for nonexisting route" do
|
67
|
+
t = Trip.new('trip', 'prod', Time.now)
|
68
|
+
id = store.add(t)
|
69
|
+
pending = store.pending_trips('nonexisting')
|
70
|
+
pending.must_equal []
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should fetch pending trips older than a point in time" do
|
74
|
+
t = Trip.new('trip', 'prod', Time.now-400)
|
75
|
+
id = store.add(t)
|
76
|
+
pending = store.pending_trips('prod', 300)
|
77
|
+
pending.first.must_equal t
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should not have side-effects" do
|
83
|
+
t = Trip.new('trip', 'prod', Time.now)
|
84
|
+
id = store.add(t)
|
85
|
+
store.remove(t).must_equal(true)
|
86
|
+
store.pending_trips('prod').must_equal []
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'roundtrip/trip'
|
4
|
+
|
5
|
+
|
6
|
+
module Roundtrip
|
7
|
+
describe Trip do
|
8
|
+
describe "::generate" do
|
9
|
+
it "should generate with a random ID if none given" do
|
10
|
+
t1 = Trip.generate("foo")
|
11
|
+
t2 = Trip.generate("foo", :invalid_option => 'poo')
|
12
|
+
t1.route.must_equal "foo"
|
13
|
+
t1.id.wont_equal t2.id
|
14
|
+
end
|
15
|
+
it "should generate with the specified ID" do
|
16
|
+
t = Trip.generate("foo", :id => 'poo')
|
17
|
+
t.route.must_equal "foo"
|
18
|
+
t.id.must_equal 'poo'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,257 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: roundtrip
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dotan Nahum
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-12-07 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: thor
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: sinatra
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: redis-namespace
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: redis
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: json
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: ratom
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :runtime
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: thor
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: statsd-ruby
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :runtime
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: guard-minitest
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ! '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
type: :development
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ! '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
- !ruby/object:Gem::Dependency
|
159
|
+
name: rr
|
160
|
+
requirement: !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ! '>='
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
none: false
|
170
|
+
requirements:
|
171
|
+
- - ! '>='
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
- !ruby/object:Gem::Dependency
|
175
|
+
name: timecop
|
176
|
+
requirement: !ruby/object:Gem::Requirement
|
177
|
+
none: false
|
178
|
+
requirements:
|
179
|
+
- - ! '>='
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
type: :development
|
183
|
+
prerelease: false
|
184
|
+
version_requirements: !ruby/object:Gem::Requirement
|
185
|
+
none: false
|
186
|
+
requirements:
|
187
|
+
- - ! '>='
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '0'
|
190
|
+
description: Simple business process/transactions tracking and metrics service
|
191
|
+
email:
|
192
|
+
- jondotan@gmail.com
|
193
|
+
executables:
|
194
|
+
- roundtrip
|
195
|
+
extensions: []
|
196
|
+
extra_rdoc_files: []
|
197
|
+
files:
|
198
|
+
- .gitignore
|
199
|
+
- Gemfile
|
200
|
+
- Guardfile
|
201
|
+
- LICENSE.txt
|
202
|
+
- Procfile
|
203
|
+
- README.md
|
204
|
+
- Rakefile
|
205
|
+
- benchmark/redis_store
|
206
|
+
- bin/roundtrip
|
207
|
+
- conf/roundtrip.yml
|
208
|
+
- config.ru
|
209
|
+
- examples/Gemfile
|
210
|
+
- examples/roundtrip-schema.png
|
211
|
+
- examples/ruby_httparty.rb
|
212
|
+
- lib/roundtrip.rb
|
213
|
+
- lib/roundtrip/cli.rb
|
214
|
+
- lib/roundtrip/core.rb
|
215
|
+
- lib/roundtrip/metrics.rb
|
216
|
+
- lib/roundtrip/metrics/statsd.rb
|
217
|
+
- lib/roundtrip/store.rb
|
218
|
+
- lib/roundtrip/store/redis.rb
|
219
|
+
- lib/roundtrip/trip.rb
|
220
|
+
- lib/roundtrip/version.rb
|
221
|
+
- lib/roundtrip/web.rb
|
222
|
+
- roundtrip.gemspec
|
223
|
+
- spec/roundtrip/core_spec.rb
|
224
|
+
- spec/roundtrip/metrics/statsd_spec.rb
|
225
|
+
- spec/roundtrip/store/redis_spec.rb
|
226
|
+
- spec/roundtrip/trip_spec.rb
|
227
|
+
- spec/spec_helper.rb
|
228
|
+
homepage: https://github.com/jondot/roundtrip
|
229
|
+
licenses: []
|
230
|
+
post_install_message:
|
231
|
+
rdoc_options: []
|
232
|
+
require_paths:
|
233
|
+
- lib
|
234
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
235
|
+
none: false
|
236
|
+
requirements:
|
237
|
+
- - ! '>='
|
238
|
+
- !ruby/object:Gem::Version
|
239
|
+
version: '0'
|
240
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
241
|
+
none: false
|
242
|
+
requirements:
|
243
|
+
- - ! '>='
|
244
|
+
- !ruby/object:Gem::Version
|
245
|
+
version: '0'
|
246
|
+
requirements: []
|
247
|
+
rubyforge_project:
|
248
|
+
rubygems_version: 1.8.23
|
249
|
+
signing_key:
|
250
|
+
specification_version: 3
|
251
|
+
summary: Simple business process/transactions tracking and metrics service
|
252
|
+
test_files:
|
253
|
+
- spec/roundtrip/core_spec.rb
|
254
|
+
- spec/roundtrip/metrics/statsd_spec.rb
|
255
|
+
- spec/roundtrip/store/redis_spec.rb
|
256
|
+
- spec/roundtrip/trip_spec.rb
|
257
|
+
- spec/spec_helper.rb
|