spectator-rb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +12 -0
- data/.travis.yml +6 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +46 -0
- data/LICENSE +202 -0
- data/OSSMETADATA +1 -0
- data/README.md +110 -0
- data/Rakefile +13 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/buildViaTravis.sh +7 -0
- data/lib/spectator.rb +22 -0
- data/lib/spectator/atomic_number.rb +49 -0
- data/lib/spectator/clock.rb +57 -0
- data/lib/spectator/counter.rb +34 -0
- data/lib/spectator/distribution_summary.rb +53 -0
- data/lib/spectator/gauge.rb +34 -0
- data/lib/spectator/http.rb +30 -0
- data/lib/spectator/measure.rb +24 -0
- data/lib/spectator/meter_id.rb +52 -0
- data/lib/spectator/registry.rb +298 -0
- data/lib/spectator/timer.rb +64 -0
- data/lib/spectator/version.rb +3 -0
- data/spectator.gemspec +30 -0
- metadata +126 -0
data/bin/setup
ADDED
data/buildViaTravis.sh
ADDED
data/lib/spectator.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spectator/version'
|
2
|
+
require 'spectator/registry'
|
3
|
+
require 'spectator/meter_id'
|
4
|
+
require 'spectator/clock'
|
5
|
+
require 'spectator/timer'
|
6
|
+
require 'spectator/counter'
|
7
|
+
require 'spectator/distribution_summary'
|
8
|
+
require 'spectator/gauge'
|
9
|
+
require 'logger'
|
10
|
+
|
11
|
+
# Simple library for instrumenting code to record dimensional time series.
|
12
|
+
module Spectator
|
13
|
+
class << self
|
14
|
+
attr_writer :logger
|
15
|
+
|
16
|
+
def logger
|
17
|
+
@logger ||= Logger.new($stdout).tap do |log|
|
18
|
+
log.progname = name
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Spectator
|
2
|
+
# Thread safe number operations
|
3
|
+
class AtomicNumber
|
4
|
+
def initialize(init)
|
5
|
+
@value = init
|
6
|
+
@lock = Mutex.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def set(value)
|
10
|
+
@lock.synchronize { @value = value }
|
11
|
+
end
|
12
|
+
|
13
|
+
def get
|
14
|
+
@lock.synchronize { @value }
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_and_set(value)
|
18
|
+
@lock.synchronize do
|
19
|
+
tmp = @value
|
20
|
+
@value = value
|
21
|
+
tmp
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_and_add(amount)
|
26
|
+
@lock.synchronize do
|
27
|
+
tmp = @value
|
28
|
+
@value += amount
|
29
|
+
tmp
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_and_get(amount)
|
34
|
+
@lock.synchronize { @value += amount }
|
35
|
+
end
|
36
|
+
|
37
|
+
def max(value)
|
38
|
+
v = value.to_f
|
39
|
+
@lock.synchronize do
|
40
|
+
@value = v if v > @value || @value.nan?
|
41
|
+
end
|
42
|
+
@value
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
"AtomicNumber{#{@value}}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Spectator
|
2
|
+
# A timing source that can be used to access the current time as an object,
|
3
|
+
# and a high resolution monotonic time
|
4
|
+
class SystemClock
|
5
|
+
# A monotonically increasing number of nanoseconds. This is useful for
|
6
|
+
# recording times, or benchmarking.
|
7
|
+
# Note that this is not guaranteed to be steady.
|
8
|
+
# In other words each tick of the underlying clock may not
|
9
|
+
# be the same length (e.g. some seconds might be longer than others)
|
10
|
+
# @return A monotonic number of nanoseconds
|
11
|
+
def monotonic_time
|
12
|
+
MonotonicTime.time_in_nanoseconds
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return a time object for the current time
|
16
|
+
def wall_time
|
17
|
+
Time.now
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# A timing source useful in unit tests that can be used to mock the methods in
|
22
|
+
# SystemClock
|
23
|
+
class ManualClock
|
24
|
+
attr_accessor :wall_time
|
25
|
+
attr_accessor :monotonic_time
|
26
|
+
|
27
|
+
# Get a new object using 2000-1-1 0:0:0 UTC as the default time,
|
28
|
+
# and 0 nanoseconds as the number of nanos reported by monotonic_time
|
29
|
+
def initialize(wall_init: Time.utc(2000, 'jan', 1, 0, 0, 0), mono_time: 0)
|
30
|
+
@wall_time = wall_init
|
31
|
+
@monotonic_time = mono_time
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Gather a monotonically increasing number of nanoseconds.
|
36
|
+
# If Process::CLOCK_MONOTONIC is available we use that, otherwise we attempt
|
37
|
+
# to use java.lang.System.nanoTime if running in jruby, and fallback
|
38
|
+
# to the Time.now implementation
|
39
|
+
module MonotonicTime
|
40
|
+
module_function
|
41
|
+
|
42
|
+
if defined? Process::CLOCK_MONOTONIC
|
43
|
+
def time_in_nanoseconds
|
44
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
45
|
+
end
|
46
|
+
elsif (defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby') == 'jruby'
|
47
|
+
def time_in_nanoseconds
|
48
|
+
java.lang.System.nanoTime
|
49
|
+
end
|
50
|
+
else
|
51
|
+
def time_in_nanoseconds
|
52
|
+
t = Time.now
|
53
|
+
t.to_i * 10**9 + t.nsec
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spectator/atomic_number'
|
2
|
+
require 'spectator/measure'
|
3
|
+
|
4
|
+
module Spectator
|
5
|
+
# A counter is used to measure the rate at which an event is occurring
|
6
|
+
class Counter
|
7
|
+
# Initialize a new instance setting its id, and starting the
|
8
|
+
# count at 0
|
9
|
+
def initialize(id)
|
10
|
+
@id = id
|
11
|
+
@count = AtomicNumber.new(0)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Increment the counter by delta
|
15
|
+
def increment(delta = 1)
|
16
|
+
@count.add_and_get(delta)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Get the current count as a list of Measure and reset the count to 0
|
20
|
+
def measure
|
21
|
+
[Measure.new(@id.with_stat('count'), @count.get_and_set(0))]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Read the current count. Calls to measure will reset it
|
25
|
+
def count
|
26
|
+
@count.get
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get a string representation for debugging purposes
|
30
|
+
def to_s
|
31
|
+
"Counter{id=#{@id}, count=#{@count.get}}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spectator/atomic_number'
|
2
|
+
|
3
|
+
module Spectator
|
4
|
+
# Track the sample distribution of events. An example would be the response
|
5
|
+
# sizes for requests hitting an http server.
|
6
|
+
#
|
7
|
+
# The class will report measurements for the total amount, the count, max,
|
8
|
+
# and the total of the square of the amounts recorded
|
9
|
+
# (useful for computing a standard deviation)
|
10
|
+
class DistributionSummary
|
11
|
+
# Initialize a new DistributionSummary instance with a given id
|
12
|
+
def initialize(id)
|
13
|
+
@id = id
|
14
|
+
@count = AtomicNumber.new(0)
|
15
|
+
@total_amount = AtomicNumber.new(0)
|
16
|
+
@total_sq = AtomicNumber.new(0)
|
17
|
+
@max = AtomicNumber.new(Float::NAN)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Update the statistics kept by the summary with the specified amount.
|
21
|
+
def record(amount)
|
22
|
+
return if amount < 0
|
23
|
+
@count.add_and_get(1)
|
24
|
+
@total_amount.add_and_get(amount)
|
25
|
+
@total_sq.add_and_get(amount * amount)
|
26
|
+
@max.max(amount)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get the current amount
|
30
|
+
def count
|
31
|
+
@count.get
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return the total amount
|
35
|
+
def total_amount
|
36
|
+
@total_amount.get
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get a list of measurements, and reset the stats
|
40
|
+
# The stats returned are the current count, the total amount,
|
41
|
+
# the sum of the square of the amounts recorded, and the max value
|
42
|
+
def measure
|
43
|
+
cnt = Measure.new(@id.with_stat('count'), @count.get_and_set(0))
|
44
|
+
tot = Measure.new(@id.with_stat('totalAmount'),
|
45
|
+
@total_amount.get_and_set(0))
|
46
|
+
tot_sq = Measure.new(@id.with_stat('totalOfSquares'),
|
47
|
+
@total_sq.get_and_set(0))
|
48
|
+
mx = Measure.new(@id.with_stat('max'), @max.get_and_set(Float::NAN))
|
49
|
+
|
50
|
+
[cnt, tot, tot_sq, mx]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spectator/atomic_number'
|
2
|
+
require 'spectator/measure'
|
3
|
+
|
4
|
+
module Spectator
|
5
|
+
# A meter with a single value that can only be sampled at a point in time.
|
6
|
+
# A typical example is a queue size.
|
7
|
+
class Gauge
|
8
|
+
# Initialize a new instance of a Gauge with the given id
|
9
|
+
def initialize(id)
|
10
|
+
@id = id
|
11
|
+
@value = AtomicNumber.new(Float::NAN)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Get the current value
|
15
|
+
def get
|
16
|
+
@value.get
|
17
|
+
end
|
18
|
+
|
19
|
+
# Set the current value to the number specified
|
20
|
+
def set(value)
|
21
|
+
@value.set(value)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Get the current value, and reset it
|
25
|
+
def measure
|
26
|
+
[Measure.new(@id.with_stat('gauge'), @value.get_and_set(Float::NAN))]
|
27
|
+
end
|
28
|
+
|
29
|
+
# A string representation of this gauge, useful for debugging purposes
|
30
|
+
def to_s
|
31
|
+
"Gauge{id=#{@id}, value=#{@value.get}}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
module Spectator
|
5
|
+
# Helper for HTTP requests
|
6
|
+
class Http
|
7
|
+
# Create a new instance using the given registry
|
8
|
+
# to record stats for the requests performed
|
9
|
+
def initialize(registry)
|
10
|
+
@registry = registry
|
11
|
+
end
|
12
|
+
|
13
|
+
# Send a JSON payload to a given endpoing
|
14
|
+
def post_json(endpoint, payload)
|
15
|
+
s = payload.to_json
|
16
|
+
uri = URI(endpoint)
|
17
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
18
|
+
req = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json')
|
19
|
+
req.body = s
|
20
|
+
begin
|
21
|
+
res = http.request(req)
|
22
|
+
rescue StandardError => e
|
23
|
+
Spectator.logger.info("Cause #{e.cause} - msg=#{e.message}")
|
24
|
+
return 400
|
25
|
+
end
|
26
|
+
|
27
|
+
res.value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Spectator
|
2
|
+
# This immutable class represents a measurement sampled from a meter
|
3
|
+
class Measure
|
4
|
+
attr_reader :id, :value
|
5
|
+
|
6
|
+
# A meter id and a value
|
7
|
+
def initialize(id, value)
|
8
|
+
@id = id
|
9
|
+
@value = value.to_f
|
10
|
+
end
|
11
|
+
|
12
|
+
# A string representation of this measurement, for debugging purposes
|
13
|
+
def to_s
|
14
|
+
"Measure{id=#{@id}, value=#{@value}}"
|
15
|
+
end
|
16
|
+
|
17
|
+
# Compare this measurement against another one,
|
18
|
+
# taking into account nan values
|
19
|
+
def ==(other)
|
20
|
+
@id == other.id && (@value == other.value ||
|
21
|
+
@value.nan? && other.value.nan?)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Spectator
|
2
|
+
# Identifier for a meter or Measure
|
3
|
+
class MeterId
|
4
|
+
attr_reader :name, :tags
|
5
|
+
def initialize(name, maybe_tags = nil)
|
6
|
+
tags = maybe_tags.nil? ? {} : maybe_tags
|
7
|
+
@name = name.to_sym
|
8
|
+
@tags = {}
|
9
|
+
tags.each { |k, v| @tags[k.to_sym] = v.to_sym }
|
10
|
+
@tags.freeze
|
11
|
+
@key = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
# Create a new MeterId with a given key and value
|
15
|
+
def with_tag(key, value)
|
16
|
+
new_tags = @tags.dup
|
17
|
+
new_tags[key] = value
|
18
|
+
MeterId.new(@name, new_tags)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Create a new MeterId with key=statistic and the given value
|
22
|
+
def with_stat(stat_value)
|
23
|
+
with_tag(:statistic, stat_value)
|
24
|
+
end
|
25
|
+
|
26
|
+
# lazyily compute a key to be used in hashes for efficiency
|
27
|
+
def key
|
28
|
+
if @key.nil?
|
29
|
+
hash_key = @name.to_s
|
30
|
+
@key = hash_key
|
31
|
+
keys = @tags.keys
|
32
|
+
keys.sort
|
33
|
+
keys.each do |k|
|
34
|
+
v = tags[k]
|
35
|
+
hash_key += "|#{k}|#{v}"
|
36
|
+
end
|
37
|
+
@key = hash_key
|
38
|
+
end
|
39
|
+
@key
|
40
|
+
end
|
41
|
+
|
42
|
+
# A string representation for debugging purposes
|
43
|
+
def to_s
|
44
|
+
"MeterId{name=#{@name}, tags=#{@tags}}"
|
45
|
+
end
|
46
|
+
|
47
|
+
# Compare our id and tags against another MeterId
|
48
|
+
def ==(other)
|
49
|
+
other.name == @name && other.tags == @tags
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,298 @@
|
|
1
|
+
require 'spectator/clock'
|
2
|
+
require 'spectator/counter'
|
3
|
+
require 'spectator/distribution_summary'
|
4
|
+
require 'spectator/gauge'
|
5
|
+
require 'spectator/http'
|
6
|
+
require 'spectator/meter_id'
|
7
|
+
require 'spectator/timer'
|
8
|
+
|
9
|
+
module Spectator
|
10
|
+
# Registry to manage a set of meters
|
11
|
+
class Registry
|
12
|
+
attr_reader :config, :clock, :publisher, :common_tags
|
13
|
+
|
14
|
+
# Initialize the registry using the given config, and clock
|
15
|
+
# The default clock is the SystemClock
|
16
|
+
# The config is a Hash which should include:
|
17
|
+
# :common_tags as a hash with tags that will be added to all metrics
|
18
|
+
# :frequency the interval at which metrics will be sent to an
|
19
|
+
# aggregator service, expressed in seconds
|
20
|
+
# :uri the endpoint for the aggregator service
|
21
|
+
def initialize(config, clock = SystemClock.new)
|
22
|
+
@config = config
|
23
|
+
@clock = clock
|
24
|
+
@meters = {}
|
25
|
+
@common_tags = to_symbols(config[:common_tags]) || {}
|
26
|
+
@lock = Mutex.new
|
27
|
+
@publisher = Publisher.new(self)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create a new MeterId with the given name, and optional tags
|
31
|
+
def new_id(name, tags = nil)
|
32
|
+
MeterId.new(name, tags)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Create or get a Counter with the given id
|
36
|
+
def counter_with_id(id)
|
37
|
+
new_meter(id) { |meter_id| Counter.new(meter_id) }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create or get a Gauge with the given id
|
41
|
+
def gauge_with_id(id)
|
42
|
+
new_meter(id) { |meter_id| Gauge.new(meter_id) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Create or get a DistributionSummary with the given id
|
46
|
+
def distribution_summary_with_id(id)
|
47
|
+
new_meter(id) { |meter_id| DistributionSummary.new(meter_id) }
|
48
|
+
end
|
49
|
+
|
50
|
+
# Create or get a Timer with the given id
|
51
|
+
def timer_with_id(id)
|
52
|
+
new_meter(id) { |meter_id| Timer.new(meter_id) }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Create or get a Counter with the given name, and optional tags
|
56
|
+
def counter(name, tags = nil)
|
57
|
+
counter_with_id(MeterId.new(name, tags))
|
58
|
+
end
|
59
|
+
|
60
|
+
# Create or get a Gauge with the given name, and optional tags
|
61
|
+
def gauge(name, tags = nil)
|
62
|
+
gauge_with_id(MeterId.new(name, tags))
|
63
|
+
end
|
64
|
+
|
65
|
+
# Create or get a DistributionSummary with the given name, and optional tags
|
66
|
+
def distribution_summary(name, tags = nil)
|
67
|
+
distribution_summary_with_id(MeterId.new(name, tags))
|
68
|
+
end
|
69
|
+
|
70
|
+
# Create or get a Timer with the given name, and optional tags
|
71
|
+
def timer(name, tags = nil)
|
72
|
+
timer_with_id(MeterId.new(name, tags))
|
73
|
+
end
|
74
|
+
|
75
|
+
# Get the list of measurements from all registered meters
|
76
|
+
def measurements
|
77
|
+
@lock.synchronize do
|
78
|
+
@meters.values.flat_map(&:measure)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Start publishing measurements to the aggregator service
|
83
|
+
def start
|
84
|
+
@publisher.start
|
85
|
+
end
|
86
|
+
|
87
|
+
# Stop publishing measurements
|
88
|
+
def stop
|
89
|
+
@publisher.stop
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def to_symbols(tags)
|
95
|
+
return nil if tags.nil?
|
96
|
+
|
97
|
+
symbolic_tags = {}
|
98
|
+
tags.each { |k, v| symbolic_tags[k.to_sym] = v.to_sym }
|
99
|
+
symbolic_tags
|
100
|
+
end
|
101
|
+
|
102
|
+
def new_meter(meter_id)
|
103
|
+
@lock.synchronize do
|
104
|
+
meter = @meters[meter_id.key]
|
105
|
+
if meter.nil?
|
106
|
+
meter = yield(meter_id)
|
107
|
+
@meters[meter_id.key] = meter
|
108
|
+
end
|
109
|
+
meter
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Internal class used to publish measurements to an aggregator service
|
115
|
+
class Publisher
|
116
|
+
def initialize(registry)
|
117
|
+
@registry = registry
|
118
|
+
@started = false
|
119
|
+
@should_stop = false
|
120
|
+
@frequency = registry.config[:frequency] || 5
|
121
|
+
@http = Http.new(registry)
|
122
|
+
end
|
123
|
+
|
124
|
+
def should_start?
|
125
|
+
if @started
|
126
|
+
Spectator.logger.info('Ignoring start request. ' \
|
127
|
+
'Spectator registry already started')
|
128
|
+
return false
|
129
|
+
end
|
130
|
+
|
131
|
+
@started = true
|
132
|
+
uri = @registry.config[:uri]
|
133
|
+
if uri.nil? || uri.empty?
|
134
|
+
Spectator.logger.info('Ignoring start request since Spectator ' \
|
135
|
+
'registry has no valid uri')
|
136
|
+
return false
|
137
|
+
end
|
138
|
+
|
139
|
+
true
|
140
|
+
end
|
141
|
+
|
142
|
+
# Start publishing if the config is acceptable:
|
143
|
+
# uri is non-nil or empty
|
144
|
+
def start
|
145
|
+
return unless should_start?
|
146
|
+
|
147
|
+
Spectator.logger.info 'Starting Spectator registry'
|
148
|
+
|
149
|
+
@should_stop = false
|
150
|
+
@publish_thread = Thread.new do
|
151
|
+
publish
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Stop publishing measurements
|
156
|
+
def stop
|
157
|
+
unless @started
|
158
|
+
Spectator.logger.info('Attemping to stop Spectator ' \
|
159
|
+
'without a previous call to start')
|
160
|
+
return
|
161
|
+
end
|
162
|
+
|
163
|
+
@should_stop = true
|
164
|
+
Spectator.logger.info('Stopping spectator')
|
165
|
+
@publish_thread.kill if @publish_thread
|
166
|
+
|
167
|
+
@started = false
|
168
|
+
Spectator.logger.info('Sending last batch of metrics before exiting')
|
169
|
+
send_metrics_now
|
170
|
+
end
|
171
|
+
|
172
|
+
ADD_OP = 0
|
173
|
+
MAX_OP = 10
|
174
|
+
UNKNOWN_OP = -1
|
175
|
+
OPS = { count: ADD_OP,
|
176
|
+
totalAmount: ADD_OP,
|
177
|
+
totalTime: ADD_OP,
|
178
|
+
totalOfSquares: ADD_OP,
|
179
|
+
percentile: ADD_OP,
|
180
|
+
max: MAX_OP,
|
181
|
+
gauge: MAX_OP,
|
182
|
+
activeTasks: MAX_OP,
|
183
|
+
duration: MAX_OP }.freeze
|
184
|
+
# Get the operation to be used for the given Measure
|
185
|
+
# Gauges are aggregated using MAX_OP, counters with ADD_OP
|
186
|
+
def op_for_measurement(measure)
|
187
|
+
stat = measure.id.tags.fetch(:statistic, :unknown)
|
188
|
+
OPS.fetch(stat, UNKNOWN_OP)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Gauges are sent if they have a value
|
192
|
+
# Counters if they have a number of increments greater than 0
|
193
|
+
def should_send(measure)
|
194
|
+
op = op_for_measurement(measure)
|
195
|
+
return measure.value > 0 if op == ADD_OP
|
196
|
+
return !measure.value.nan? if op == MAX_OP
|
197
|
+
|
198
|
+
false
|
199
|
+
end
|
200
|
+
|
201
|
+
# Build a string table from the list of measurements
|
202
|
+
# Unique words are identified, and assigned a number starting from 0 based
|
203
|
+
# on their lexicographical order
|
204
|
+
def build_string_table(measurements)
|
205
|
+
common_tags = @registry.common_tags
|
206
|
+
table = {}
|
207
|
+
common_tags.each do |k, v|
|
208
|
+
table[k] = 0
|
209
|
+
table[v] = 0
|
210
|
+
end
|
211
|
+
table[:name] = 0
|
212
|
+
measurements.each do |m|
|
213
|
+
table[m.id.name] = 0
|
214
|
+
m.id.tags.each do |k, v|
|
215
|
+
table[k] = 0
|
216
|
+
table[v] = 0
|
217
|
+
end
|
218
|
+
end
|
219
|
+
keys = table.keys.sort
|
220
|
+
keys.each_with_index do |str, index|
|
221
|
+
table[str] = index
|
222
|
+
end
|
223
|
+
table
|
224
|
+
end
|
225
|
+
|
226
|
+
# Add a measurement to our payload table.
|
227
|
+
# The serialization for a measurement is:
|
228
|
+
# - length of tags
|
229
|
+
# - indexes for the tags based on the string table
|
230
|
+
# - operation (add (0), max (10))
|
231
|
+
# - floating point value
|
232
|
+
def append_measurement(payload, table, measure)
|
233
|
+
op = op_for_measurement(measure)
|
234
|
+
common_tags = @registry.common_tags
|
235
|
+
tags = measure.id.tags
|
236
|
+
len = tags.length + 1 + common_tags.length
|
237
|
+
payload.push(len)
|
238
|
+
common_tags.each do |k, v|
|
239
|
+
payload.push(table[k])
|
240
|
+
payload.push(table[v])
|
241
|
+
end
|
242
|
+
tags.each do |k, v|
|
243
|
+
payload.push(table[k])
|
244
|
+
payload.push(table[v])
|
245
|
+
end
|
246
|
+
payload.push(table[:name])
|
247
|
+
payload.push(table[measure.id.name])
|
248
|
+
payload.push(op)
|
249
|
+
payload.push(measure.value)
|
250
|
+
end
|
251
|
+
|
252
|
+
# Generate a payload from the list of measurements
|
253
|
+
# The payload is an array, with the number of elements in the string table
|
254
|
+
# The string table, and measurements
|
255
|
+
def payload_for_measurements(measurements)
|
256
|
+
table = build_string_table(measurements)
|
257
|
+
payload = []
|
258
|
+
payload.push(table.length)
|
259
|
+
strings = table.keys.sort
|
260
|
+
payload.concat(strings)
|
261
|
+
measurements.each { |m| append_measurement(payload, table, m) }
|
262
|
+
payload
|
263
|
+
end
|
264
|
+
|
265
|
+
# Get a list of measurements that should be sent
|
266
|
+
def registry_measurements
|
267
|
+
@registry.measurements.select { |m| should_send(m) }
|
268
|
+
end
|
269
|
+
|
270
|
+
# Send the current measurements to our aggregator service
|
271
|
+
def send_metrics_now
|
272
|
+
ms = registry_measurements
|
273
|
+
if ms.empty?
|
274
|
+
Spectator.logger.debug 'No measurements to send'
|
275
|
+
else
|
276
|
+
payload = payload_for_measurements(ms)
|
277
|
+
uri = @registry.config[:uri]
|
278
|
+
Spectator.logger.info "Sending #{ms.length} measurements to #{uri}"
|
279
|
+
@http.post_json(uri, payload)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Publish loop:
|
284
|
+
# send measurements to the aggregator endpoint ':uri',
|
285
|
+
# every ':frequency' seconds
|
286
|
+
def publish
|
287
|
+
clock = @registry.clock
|
288
|
+
until @should_stop
|
289
|
+
start = clock.wall_time
|
290
|
+
Spectator.logger.info 'Publishing'
|
291
|
+
send_metrics_now
|
292
|
+
elapsed = clock.wall_time - start
|
293
|
+
sleep @frequency - elapsed if elapsed < @frequency
|
294
|
+
end
|
295
|
+
Spectator.logger.info 'Stopping publishing thread'
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|