spectator-rb 0.1.0
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 +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
|