invoca-metrics 0.0.2 → 1.14.1
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.
Potentially problematic release.
This version of invoca-metrics might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/LICENSE.txt +22 -0
- data/README.md +161 -0
- data/invoca-metrics.gemspec +34 -0
- data/lib/invoca/metrics/batch.rb +38 -0
- data/lib/invoca/metrics/client.rb +171 -0
- data/lib/invoca/metrics/direct_metric.rb +68 -0
- data/lib/invoca/metrics/gauge_cache.rb +80 -0
- data/lib/invoca/metrics/prometheus/configuration.rb +109 -0
- data/lib/invoca/metrics/prometheus/declare_metrics/base.rb +52 -0
- data/lib/invoca/metrics/prometheus/declare_metrics/counter.rb +26 -0
- data/lib/invoca/metrics/prometheus/declare_metrics/dsl.rb +61 -0
- data/lib/invoca/metrics/prometheus/declare_metrics/gauge.rb +32 -0
- data/lib/invoca/metrics/prometheus/declare_metrics/histogram.rb +50 -0
- data/lib/invoca/metrics/prometheus/metrics_registry.rb +43 -0
- data/lib/invoca/metrics/prometheus.rb +67 -0
- data/lib/invoca/metrics/statsd_client.rb +49 -0
- data/lib/invoca/metrics/version.rb +7 -0
- data/lib/invoca/metrics.rb +117 -0
- metadata +89 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 90b8517fa182fbf84e8067d0f4727aac492ea493804f5913cbd0495a3c1a1a88
|
4
|
+
data.tar.gz: f694ecc164dc329381989fbce06d5ea91fb416707d54ca9a571eeda286c3ab52
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: db2b4c7f76eb190ce6e2ff8231f5eb1f39103fde83e314d4eb40d18c1b01a73820e0b253be0969bdcbf179ea8cbdb4466845182c952ee1a159f736486bd041ed
|
7
|
+
data.tar.gz: b48fd3176d18a7957c6cf98e88513044c275195f8ab93036192f0599b9c338d84c615b94ad8465f468318f4513ecd42fca381a2ef180d17541c09b352c689a2e
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Cary Penniman
|
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/README.md
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
# Invoca::Metrics
|
2
|
+
|
3
|
+
Publish metrics for observability!
|
4
|
+
|
5
|
+
## Dependencies
|
6
|
+
* Ruby >= 2.6
|
7
|
+
* ActiveSupport >= 4.2, < 7
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
Mix in the `Invoca::Metrics::Source` module:
|
12
|
+
```ruby
|
13
|
+
class MyClass
|
14
|
+
include Invoca::Metrics::Source
|
15
|
+
...
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
Then declare the metrics that can be published by the class using a `declare_metrics` DSL block.
|
20
|
+
Each entry in the block starts with the metric type as the DSL method:
|
21
|
+
|
22
|
+
| Metric Type | Value | Metric Instance Methods |
|
23
|
+
|:-----------:|:----------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
24
|
+
| `counter` | monotonically incrementing floating point | `.increment(value = 1, **labels)` |
|
25
|
+
| `gauge` | arbitrary floating point | `.set(value, **labels)` |
|
26
|
+
| `histogram` | histogram counters with configurable bucket cutoffs | `.add(value, **labels)`<br/><br/>`.time(return_timing: false, **labels, &block)`<br/>yields to block and `add()`s the milliseconds spent to the histogram; returns either `block_value` or `[milliseconds, block_value]` depending on the `return_timing` argument |
|
27
|
+
|
28
|
+
After the metric type, the first argument is the metric name, given as a symbol. This is globally unique.
|
29
|
+
Note that other classes in the process are allowed to declare the same metric, as long as the type and labels are the same.
|
30
|
+
|
31
|
+
Each metric may have any number of labels declared in the optional `labels:` hash, with default values in case that label is not passed when the metric is published.
|
32
|
+
|
33
|
+
Note: An implicit label of `service: <service_name>` is always added. The `<service_name>` is a configured value.
|
34
|
+
|
35
|
+
Histograms have a required argument, `buckets: <array>`, where the array is the histogram bucket cutoffs.
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
class MyClass
|
39
|
+
include Invoca::Metrics::Source
|
40
|
+
|
41
|
+
declare_metrics do
|
42
|
+
counter :request_total, labels: { status: '200' }
|
43
|
+
gauge :available_memory
|
44
|
+
histogram :processing_time, buckets: [30.seconds, 1.hour, 2.hours, 4.hours, 5.hours, 6.hours, 10.hours]
|
45
|
+
end
|
46
|
+
|
47
|
+
...
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
After declaring the metrics, you may use `prometheus_metrics.<metric_name>` (in either the class or instance) to publish the metric.
|
52
|
+
```ruby
|
53
|
+
prometheus_metrics.request_total.increment # increment the counter by 1
|
54
|
+
|
55
|
+
prometheus_metrics.request_total.increment(5) # increment the counter by 5
|
56
|
+
|
57
|
+
prometheus_metrics.available_memory.set(10.25) # set the gauge to 10.25
|
58
|
+
|
59
|
+
prometheus_metrics.processing_time.add(100) # Increment the bucket closest to the value 100
|
60
|
+
prometheus_metrics.processing_time.time { work } # measure the elapsed milliseconds to run the given block and increment the bucket closest to that time period in milliseconds
|
61
|
+
```
|
62
|
+
|
63
|
+
## Installation
|
64
|
+
|
65
|
+
Add this line to your application's Gemfile:
|
66
|
+
|
67
|
+
gem 'invoca-metrics'
|
68
|
+
|
69
|
+
And then execute:
|
70
|
+
|
71
|
+
$ bundle
|
72
|
+
|
73
|
+
Or install it yourself as:
|
74
|
+
|
75
|
+
$ gem install invoca-metrics
|
76
|
+
|
77
|
+
## Setup
|
78
|
+
|
79
|
+
### Default Setup
|
80
|
+
|
81
|
+
Add the following code to your application...
|
82
|
+
|
83
|
+
require 'invoca/metrics'
|
84
|
+
|
85
|
+
Invoca::Metrics.service_name = "tts-service"
|
86
|
+
Invoca::Metrics.server_name = Socket.gethostname
|
87
|
+
Invoca::Metrics.cluster_name = "production"
|
88
|
+
Invoca::Metrics.sub_server_name = "worker_process_1"
|
89
|
+
Invoca::Metrics.statsd_host = "255.0.0.123"
|
90
|
+
Invoca::Metrics.statsd_port = 200
|
91
|
+
|
92
|
+
Invoca::Metrics::Client.logger = logger
|
93
|
+
Invoca::Metrics::Client.log_send_failures = ['staging', 'production'].include(environment)
|
94
|
+
|
95
|
+
Note: the `service_name` setting is required; all others are optional.
|
96
|
+
|
97
|
+
### Multi Configuration
|
98
|
+
|
99
|
+
To set multiple configurations, supply a `config` hash with additional settings.
|
100
|
+
|
101
|
+
Invoca::Metrics.config = {
|
102
|
+
deployment_group: {
|
103
|
+
server_name: "deployment"
|
104
|
+
statsd_host: "255.0.0.456",
|
105
|
+
statsd_port: 300
|
106
|
+
},
|
107
|
+
region: {
|
108
|
+
server_name: "region_name"
|
109
|
+
statsd_host: "255.0.0.789",
|
110
|
+
statsd_port: 400,
|
111
|
+
sub_server_name: nil
|
112
|
+
cluster_name: nil
|
113
|
+
}
|
114
|
+
}
|
115
|
+
|
116
|
+
The settings (`[service_name, server_name, cluster_name, sub_server_name, statsd_host, statsd_port]`) directly set on `Invoca::Metrics` will be the default values supplied if the individual configuration does not supply the option.
|
117
|
+
It's highly suggested that each configuration has its own `statsd_host` and `statsd_port` along with unique naming since writing the same metric from one statsd node could be overwritten by the same metric from a separate node.
|
118
|
+
|
119
|
+
In order to set a configuration as the default, set the configuration key as `default_config_key`.
|
120
|
+
|
121
|
+
Invoca::Metrics.default_config_key = :deployment_group
|
122
|
+
|
123
|
+
Any keys missing from the `default_config_key` config will by default have the values from the keys set directly on `Invoca::Metrics`.
|
124
|
+
|
125
|
+
The full set of default values for the above example would then be
|
126
|
+
|
127
|
+
service_name: "my_event_worker"
|
128
|
+
cluster_name: "production"
|
129
|
+
sub_server_name: "worker_process_1"
|
130
|
+
server_name: "deployment"
|
131
|
+
statsd_host: "255.0.0.456",
|
132
|
+
statsd_port: 300
|
133
|
+
|
134
|
+
### Prometheus Metric Exporting Setup
|
135
|
+
There are 2 ways to set up metrics for being scraped by Prometheus: `:direct` and `:push`).
|
136
|
+
`:direct` will expose a web `server directly from the running process in order for metrics to be scraped while `:push` will push metrics over to an aggregate gateway which in turn implements a web server.
|
137
|
+
|
138
|
+
#### `:direct` Configuration Example
|
139
|
+
```ruby
|
140
|
+
Invoca::Metrics::Prometheus.configure do |config|
|
141
|
+
config.mode = :direct
|
142
|
+
config.port = 9394 # Port to expose metrics from
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
#### `:push` Configuration
|
147
|
+
```ruby
|
148
|
+
Invoca::Metrics::Prometheus.configure do |config|
|
149
|
+
config.mode = :push
|
150
|
+
config.host = '127.0.0.1' # IP address to push metrics to
|
151
|
+
config.port = 9394 # Port to push metrics to
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
## Contributing
|
156
|
+
|
157
|
+
1. Fork it
|
158
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
159
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
160
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
161
|
+
5. Create new Pull Request
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'invoca/metrics/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "invoca-metrics"
|
9
|
+
spec.version = Invoca::Metrics::VERSION
|
10
|
+
spec.authors = ["Invoca development"]
|
11
|
+
spec.email = ["development@invoca.com"]
|
12
|
+
spec.description = 'Invoca metrics reporting library'
|
13
|
+
spec.summary = 'Invoca metrics reporting library'
|
14
|
+
spec.homepage = "https://github.com/Invoca/invoca-metrics"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
18
|
+
|
19
|
+
spec.files = [
|
20
|
+
*Dir.glob('lib/**/*.rb'),
|
21
|
+
'README.md',
|
22
|
+
'LICENSE.txt',
|
23
|
+
'invoca-metrics.gemspec',
|
24
|
+
]
|
25
|
+
|
26
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
27
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_dependency "activesupport", ">= 4.2", "< 7"
|
31
|
+
spec.add_dependency "statsd-ruby", "~> 1.2.1"
|
32
|
+
spec.add_dependency "prometheus_exporter", "~> 2.0"
|
33
|
+
spec.add_dependency "contextual_logger", "~> 1.0"
|
34
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext/module/delegation'
|
5
|
+
|
6
|
+
module Invoca
|
7
|
+
module Metrics
|
8
|
+
class Batch < Client
|
9
|
+
delegate :batch_size, :batch_size=, to: :statsd_client
|
10
|
+
|
11
|
+
# @param [Invoca::Metrics::Client] client requires a configured Client instance
|
12
|
+
# @param [Statsd::Batch] statsd_batch requires a configured Batch instance
|
13
|
+
def initialize(client, statsd_batch)
|
14
|
+
super(
|
15
|
+
hostname: client.hostname,
|
16
|
+
port: client.port,
|
17
|
+
cluster_name: client.cluster_name,
|
18
|
+
service_name: client.service_name,
|
19
|
+
server_label: client.server_label,
|
20
|
+
sub_server_name: client.sub_server_name,
|
21
|
+
namespace: client.namespace
|
22
|
+
)
|
23
|
+
@statsd_client = statsd_batch
|
24
|
+
end
|
25
|
+
|
26
|
+
# @yields [Batch] yields itself
|
27
|
+
#
|
28
|
+
# A convenience method to ensure that data is not lost in the event of an
|
29
|
+
# exception being thrown. Batches will be transmitted on the parent socket
|
30
|
+
# as soon as the batch is full, and when the block finishes.
|
31
|
+
def ensure_send
|
32
|
+
yield self
|
33
|
+
ensure
|
34
|
+
statsd_client.flush
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext/module/delegation'
|
5
|
+
|
6
|
+
module Invoca
|
7
|
+
module Metrics
|
8
|
+
class Client
|
9
|
+
STATSD_DEFAULT_HOSTNAME = "127.0.0.1"
|
10
|
+
STATSD_DEFAULT_PORT = 8125
|
11
|
+
STATSD_METRICS_SEPARATOR = '.'
|
12
|
+
|
13
|
+
class << self
|
14
|
+
delegate :logger, :logger=, :log_send_failures, :log_send_failures=, to: StatsdClient
|
15
|
+
|
16
|
+
# Default values are required for backwards compatibility
|
17
|
+
def metrics(statsd_host: Invoca::Metrics.default_client_config[:statsd_host],
|
18
|
+
statsd_port: Invoca::Metrics.default_client_config[:statsd_port],
|
19
|
+
cluster_name: Invoca::Metrics.default_client_config[:cluster_name],
|
20
|
+
service_name: Invoca::Metrics.default_client_config[:service_name],
|
21
|
+
server_name: Invoca::Metrics.default_client_config[:server_name],
|
22
|
+
sub_server_name: Invoca::Metrics.default_client_config[:sub_server_name],
|
23
|
+
namespace: nil)
|
24
|
+
config = {
|
25
|
+
hostname: statsd_host || STATSD_DEFAULT_HOSTNAME,
|
26
|
+
port: statsd_port || STATSD_DEFAULT_PORT,
|
27
|
+
cluster_name: cluster_name,
|
28
|
+
service_name: service_name,
|
29
|
+
server_label: server_name,
|
30
|
+
sub_server_name: sub_server_name,
|
31
|
+
namespace: namespace
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
client_cache[config] ||= new(**config)
|
35
|
+
end
|
36
|
+
|
37
|
+
def reset_cache
|
38
|
+
@client_cache = {}
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def client_cache
|
44
|
+
@client_cache ||= {}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_reader :hostname, :port, :server_label, :sub_server_name, :cluster_name, :service_name, :gauge_cache
|
49
|
+
delegate :batch_size, :namespace, :timing, :time, to: :statsd_client
|
50
|
+
|
51
|
+
def initialize(hostname:, port:, cluster_name: nil, service_name: nil, server_label: nil, sub_server_name: nil, namespace: nil)
|
52
|
+
@hostname = hostname
|
53
|
+
@port = port
|
54
|
+
@cluster_name = cluster_name
|
55
|
+
@service_name = service_name
|
56
|
+
@server_label = server_label
|
57
|
+
@sub_server_name = sub_server_name
|
58
|
+
|
59
|
+
@statsd_client = StatsdClient.new(@hostname, @port)
|
60
|
+
@statsd_client.namespace = namespace || [@cluster_name, @service_name].compact.join(STATSD_METRICS_SEPARATOR).presence
|
61
|
+
|
62
|
+
@gauge_cache = GaugeCache.register(gauge_cache_key, @statsd_client)
|
63
|
+
end
|
64
|
+
|
65
|
+
def gauge_cache_key
|
66
|
+
[
|
67
|
+
hostname,
|
68
|
+
port,
|
69
|
+
cluster_name,
|
70
|
+
service_name,
|
71
|
+
namespace,
|
72
|
+
server_name,
|
73
|
+
sub_server_name
|
74
|
+
].freeze
|
75
|
+
end
|
76
|
+
|
77
|
+
def server_name # For backwards compatibility
|
78
|
+
server_label
|
79
|
+
end
|
80
|
+
|
81
|
+
# This will store the gauge value passed in so that it is reported every GAUGE_REPORT_INTERVAL
|
82
|
+
# seconds and post the gauge at the same time to avoid delay in gauges being
|
83
|
+
def gauge(name, value)
|
84
|
+
log_usage(name, :gauge)
|
85
|
+
if (args = normalized_metric_name_and_value(name, value, "gauge"))
|
86
|
+
gauge_cache.set(*args)
|
87
|
+
statsd_client.gauge(*args)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def count(name, value = 1)
|
92
|
+
log_usage(name, :counter)
|
93
|
+
if (args = normalized_metric_name_and_value(name, value, "counter"))
|
94
|
+
statsd_client.count(*args)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
alias counter count
|
99
|
+
|
100
|
+
def increment(name)
|
101
|
+
count(name, 1)
|
102
|
+
end
|
103
|
+
|
104
|
+
def decrement(name)
|
105
|
+
count(name, -1)
|
106
|
+
end
|
107
|
+
|
108
|
+
def set(name, value)
|
109
|
+
log_usage(name, :counter)
|
110
|
+
if (args = normalized_metric_name_and_value(name, value, nil))
|
111
|
+
statsd_client.set(*args)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def timer(name, milliseconds = nil, return_timing: false, &block)
|
116
|
+
name.present? or raise ArgumentError, "Must specify a metric name."
|
117
|
+
(!milliseconds.nil? ^ block_given?) or raise ArgumentError, "Must pass exactly one of milliseconds or block."
|
118
|
+
name_and_type = [name, "timer", server_label].join(STATSD_METRICS_SEPARATOR)
|
119
|
+
log_usage(name, :timer)
|
120
|
+
|
121
|
+
if milliseconds.nil?
|
122
|
+
result, block_time = time(name_and_type, &block)
|
123
|
+
return_timing ? [result, block_time] : result
|
124
|
+
else
|
125
|
+
timing(name_and_type, milliseconds)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def batch(&block)
|
130
|
+
statsd_client.batch do |batch|
|
131
|
+
Metrics::Batch.new(self, batch).ensure_send(&block)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# TODO: - implement transmit method
|
136
|
+
def transmit(message, extra_data = {})
|
137
|
+
# TODO: - we need to wire up exception data to a monitoring service
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
attr_reader :statsd_client
|
143
|
+
|
144
|
+
def normalized_metric_name_and_value(name, value, stat_type)
|
145
|
+
name.present? or raise ArgumentError, "Must specify a metric name."
|
146
|
+
extended_name = [name, stat_type, @server_label, @sub_server_name].compact.join(STATSD_METRICS_SEPARATOR)
|
147
|
+
if value
|
148
|
+
[extended_name, value]
|
149
|
+
else
|
150
|
+
[extended_name]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def log_usage(metric_name, metric_type)
|
155
|
+
if Invoca::Metrics.graphite_usage_logging_enabled
|
156
|
+
call_stack = caller_locations
|
157
|
+
unless call_stack.any? { |l| /invoca\/metrics\/prometheus/.match?(l.to_s) }
|
158
|
+
self.class.logger&.info(
|
159
|
+
"Deprecated usage of grapihite metrics",
|
160
|
+
invoca_metrics: {
|
161
|
+
metric_name: metric_name,
|
162
|
+
metric_type: metric_type,
|
163
|
+
metric_source: call_stack.find { |l| !/invoca\/metrics/.match?(l.to_s) }.to_s
|
164
|
+
}
|
165
|
+
)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Invoca
|
4
|
+
module Metrics
|
5
|
+
# Directly reports metrics without sending through graphite. Does not add process information to metric names.
|
6
|
+
class DirectMetric
|
7
|
+
attr_reader :name, :value, :tick
|
8
|
+
|
9
|
+
def initialize(name, value, tick = nil)
|
10
|
+
@name = name
|
11
|
+
@value = value
|
12
|
+
@tick = tick || self.class.rounded_tick
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
"#{name} #{value} #{tick}"
|
17
|
+
end
|
18
|
+
|
19
|
+
PERIOD = 60
|
20
|
+
DEFAULT_PORT = 2003
|
21
|
+
DEFAULT_HOST = '127.0.0.1'
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def report(metrics)
|
25
|
+
metrics_output = Array(metrics).map { |metric| "#{metric}\n" }.join
|
26
|
+
|
27
|
+
send_to_metric_host(metrics_output)
|
28
|
+
end
|
29
|
+
|
30
|
+
def generate_distribution(metric_prefix, metric_values, tick = nil)
|
31
|
+
fixed_tick = tick || rounded_tick
|
32
|
+
sorted_values = metric_values.sort
|
33
|
+
count = sorted_values.count
|
34
|
+
|
35
|
+
if count == 0
|
36
|
+
[
|
37
|
+
new("#{metric_prefix}.count", count, fixed_tick)
|
38
|
+
]
|
39
|
+
else
|
40
|
+
[
|
41
|
+
new("#{metric_prefix}.count", count, fixed_tick),
|
42
|
+
new("#{metric_prefix}.max", sorted_values[-1], fixed_tick),
|
43
|
+
new("#{metric_prefix}.min", sorted_values[0], fixed_tick),
|
44
|
+
new("#{metric_prefix}.median", sorted_values[count * 0.5], fixed_tick),
|
45
|
+
new("#{metric_prefix}.upper_90", sorted_values[count * 0.9], fixed_tick)
|
46
|
+
]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def rounded_tick
|
51
|
+
tick = Time.now.to_i
|
52
|
+
tick - (tick % PERIOD)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def send_to_metric_host(message)
|
58
|
+
host = ENV["DIRECT_METRIC_HOST"] || DEFAULT_HOST
|
59
|
+
port = (ENV["DIRECT_METRIC_PORT"] || DEFAULT_PORT).to_i
|
60
|
+
|
61
|
+
TCPSocket.open(host, port) do |tcp|
|
62
|
+
tcp.send(message, 0)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Invoca
|
4
|
+
module Metrics
|
5
|
+
class GaugeCache
|
6
|
+
GAUGE_REPORT_INTERVAL = 60.seconds
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def register(cache_key, statsd_client)
|
10
|
+
registered_gauge_caches[cache_key] ||= new(statsd_client)
|
11
|
+
end
|
12
|
+
|
13
|
+
def reset
|
14
|
+
@registered_gauge_caches = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def registered_gauge_caches
|
20
|
+
@registered_gauge_caches ||= {}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :cache
|
25
|
+
|
26
|
+
def initialize(statsd_client)
|
27
|
+
@statsd_client = statsd_client
|
28
|
+
@cache = {}
|
29
|
+
start_reporting_thread
|
30
|
+
end
|
31
|
+
|
32
|
+
# Atomic method for setting the value for a particular gauge
|
33
|
+
def set(metric, value)
|
34
|
+
@cache[metric] = value
|
35
|
+
end
|
36
|
+
|
37
|
+
# Reports all gauges that have been set in the cache
|
38
|
+
# To avoid "RuntimeError: can't add a new key into hash during iteration" from occurring we are
|
39
|
+
# temporarily duplicating the cache to iterate and send the batch of metrics
|
40
|
+
def report
|
41
|
+
@statsd_client.batch do |statsd_batch|
|
42
|
+
@cache.dup.each do |metric, value|
|
43
|
+
statsd_batch.gauge(metric, value) if value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def service_environment
|
49
|
+
@service_environment ||= ENV['RAILS_ENV'].presence || ENV['SERVICE_ENV'].presence || 'development'
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def start_reporting_thread
|
55
|
+
Thread.new do
|
56
|
+
reporting_loop_with_rescue
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def reporting_loop_with_rescue
|
61
|
+
reporting_loop
|
62
|
+
rescue Exception => ex
|
63
|
+
Invoca::Metrics::Client.logger.error("GaugeCache#reporting_loop_with_rescue rescued exception:\n#{ex.class}: #{ex.message}")
|
64
|
+
end
|
65
|
+
|
66
|
+
def reporting_loop
|
67
|
+
next_time = Time.now.to_i
|
68
|
+
loop do
|
69
|
+
next_time = (((next_time + GAUGE_REPORT_INTERVAL + 1) / GAUGE_REPORT_INTERVAL) * GAUGE_REPORT_INTERVAL) - 1
|
70
|
+
report
|
71
|
+
if (delay = next_time - Time.now.to_i) > 0
|
72
|
+
sleep(delay)
|
73
|
+
else
|
74
|
+
warn("Window to report gauge may have been missed.") unless service_environment == 'test'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'prometheus_exporter'
|
4
|
+
require 'prometheus_exporter/server'
|
5
|
+
require 'prometheus_exporter/client'
|
6
|
+
|
7
|
+
module Invoca
|
8
|
+
module Metrics
|
9
|
+
module Prometheus
|
10
|
+
class Configuration
|
11
|
+
AVAILABLE_MODES = [:direct, :push].freeze
|
12
|
+
AVAILABLE_METRIC_TYPES = [:counter, :gauge, :histogram].freeze
|
13
|
+
|
14
|
+
# The mode to use for exporting Prometheus metrics.
|
15
|
+
# Valid modes are:
|
16
|
+
# - `:direct` (default): Exports metrics directly using a threaded webserver.
|
17
|
+
# - `:push`: Exports metrics using a pushgateway.
|
18
|
+
#
|
19
|
+
# @return [Symbol]
|
20
|
+
attr_reader :mode
|
21
|
+
|
22
|
+
# The host to use for the Prometheus server. This setting
|
23
|
+
# is only used when in :push mode. Defaults to `localhost`.
|
24
|
+
#
|
25
|
+
# @return [String]
|
26
|
+
attr_accessor :host
|
27
|
+
|
28
|
+
# The port to use when exporting metrics. When in :direct mode
|
29
|
+
# this is used to specify the port to listen on. When in :push
|
30
|
+
# mode this is used to specify the port to push to. Defaults to 9394.
|
31
|
+
#
|
32
|
+
# @return [Integer]
|
33
|
+
attr_accessor :port
|
34
|
+
|
35
|
+
# The client to use for exporting Prometheus metrics.
|
36
|
+
#
|
37
|
+
# @return [PrometheusExporter::Client]
|
38
|
+
attr_reader :client
|
39
|
+
|
40
|
+
# The webserver that is exposed when in :direct mode.
|
41
|
+
#
|
42
|
+
# @return [PrometheusExporter::Server::WebServer]
|
43
|
+
attr_reader :server
|
44
|
+
|
45
|
+
def initialize
|
46
|
+
@host = 'localhost'
|
47
|
+
@port = 9394
|
48
|
+
end
|
49
|
+
|
50
|
+
def mode=(new_mode)
|
51
|
+
AVAILABLE_MODES.include?(new_mode) or raise ArgumentError, "Invalid mode #{new_mode.inspect}. Allowed modes: #{AVAILABLE_MODES}"
|
52
|
+
@mode = new_mode
|
53
|
+
end
|
54
|
+
|
55
|
+
# Initializes the client to use for exporting Prometheus metrics and
|
56
|
+
# freezes the configuration object so no more changes can be made to it.
|
57
|
+
def freeze
|
58
|
+
validate_configuration!
|
59
|
+
|
60
|
+
case mode
|
61
|
+
when :direct
|
62
|
+
initialize_direct_client
|
63
|
+
when :push
|
64
|
+
initialize_push_client
|
65
|
+
end
|
66
|
+
|
67
|
+
PrometheusExporter::Client.default = @client
|
68
|
+
|
69
|
+
super
|
70
|
+
end
|
71
|
+
|
72
|
+
# Registers a metric against the current Configuration. Once the metric object
|
73
|
+
# is registered, metrics can be reported and exported through the metric object.
|
74
|
+
#
|
75
|
+
# @param type [Symbol] The type of metric to register. Valid types are: :counter, :gauge, :histogram
|
76
|
+
# @param name [String] The name of the metric.
|
77
|
+
# @param help [String] The help text for the metric.
|
78
|
+
# @param opts [Hash] A hash of options to pass to the metric object. This is different depending on the
|
79
|
+
# type of metrics being registered.
|
80
|
+
#
|
81
|
+
# @return metric [PrometheusExporter::Metric]
|
82
|
+
def register_metric(type, name, help, opts = nil)
|
83
|
+
AVAILABLE_METRIC_TYPES.include?(type) or raise ArgumentError, "Invalid metric type #{type.inspect}. Allowed types: #{AVAILABLE_METRIC_TYPES}"
|
84
|
+
client.register(type, name, help, opts)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def initialize_direct_client
|
90
|
+
@server = PrometheusExporter::Server::WebServer.new(bind: '0.0.0.0', port: port)
|
91
|
+
@server.start
|
92
|
+
@client = PrometheusExporter::LocalClient.new(collector: @server.collector)
|
93
|
+
end
|
94
|
+
|
95
|
+
def initialize_push_client
|
96
|
+
@client = PrometheusExporter::Client.new(host: host, port: port)
|
97
|
+
end
|
98
|
+
|
99
|
+
def validate_configuration!
|
100
|
+
mode.present? or raise ArgumentError, "Mode must be provided. Valid modes: #{AVAILABLE_MODES}"
|
101
|
+
port.present? or raise ArgumentError, "Port must be provided when in #{mode.inspect} mode"
|
102
|
+
if mode == :push
|
103
|
+
host.present? or raise ArgumentError, "Host must be provided when in #{mode.inspect} mode"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Invoca
|
4
|
+
module Metrics
|
5
|
+
module Prometheus
|
6
|
+
module DeclareMetrics
|
7
|
+
class Base
|
8
|
+
|
9
|
+
attr_reader :name, :type, :source, :metric, :default_labels, :graphite
|
10
|
+
|
11
|
+
def initialize(name, type, source, metric, labels:, graphite:)
|
12
|
+
@name = name
|
13
|
+
@type = type
|
14
|
+
@source = source
|
15
|
+
@metric = metric
|
16
|
+
@default_labels = labels
|
17
|
+
@graphite = graphite
|
18
|
+
|
19
|
+
if labels
|
20
|
+
labels.is_a?(Hash) or raise ArgumentError, "Labels must be type Hash but got #{labels.inspect}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# TODO: Could default_labels be renamed to labels?
|
25
|
+
def settings
|
26
|
+
{ type: type, labels: default_labels, graphite: graphite }
|
27
|
+
end
|
28
|
+
|
29
|
+
# TODO: This is temporary; the invoca-metrics metrics objects
|
30
|
+
# should be made immutable, in which case the entire object
|
31
|
+
# would be replaced rather than resetting the Prometheus metric
|
32
|
+
# object as is done here
|
33
|
+
def reset_metric(new_metric)
|
34
|
+
@metric = new_metric
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def render_graphite_string(**labels)
|
40
|
+
rendered_string = graphite.dup
|
41
|
+
labels.map do |key, value|
|
42
|
+
rendered_string.gsub!(":#{key}", value.to_s)
|
43
|
+
end
|
44
|
+
|
45
|
+
rendered_string.include?(':') and warn("Graphite string #{rendered_string} not fully rendered. Expecting additional label that has not been rendered.")
|
46
|
+
rendered_string
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module Invoca
|
6
|
+
module Metrics
|
7
|
+
module Prometheus
|
8
|
+
module DeclareMetrics
|
9
|
+
class Counter < Base
|
10
|
+
|
11
|
+
def increment(value = 1, **labels)
|
12
|
+
if metric.present?
|
13
|
+
metric.increment(default_labels.merge(labels), value)
|
14
|
+
else
|
15
|
+
warn("Counter being incremented without metric being present")
|
16
|
+
end
|
17
|
+
|
18
|
+
if graphite
|
19
|
+
Invoca::Metrics::Client.metrics.count(render_graphite_string(**default_labels.merge(labels)), value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../configuration'
|
4
|
+
require_relative 'counter'
|
5
|
+
require_relative 'gauge'
|
6
|
+
require_relative 'histogram'
|
7
|
+
|
8
|
+
module Invoca
|
9
|
+
module Metrics
|
10
|
+
module Prometheus
|
11
|
+
module DeclareMetrics
|
12
|
+
class Dsl
|
13
|
+
|
14
|
+
attr_reader :declared_metrics, :source, :prometheus_exporter_config
|
15
|
+
|
16
|
+
def initialize(source)
|
17
|
+
@source = source
|
18
|
+
@declared_metrics = []
|
19
|
+
if Invoca::Metrics::Prometheus.config_present?
|
20
|
+
@prometheus_exporter_config = Invoca::Metrics::Prometheus.config
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def counter(name, labels: {}, graphite: nil, help: nil)
|
25
|
+
# TODO: We can drop the 'to_s' when we stop supporting ruby 2.6
|
26
|
+
if !name.to_s.end_with?("_total")
|
27
|
+
warn("#{source} counter #{name.inspect} should end in \"_total\" so that Grafana can tell that it is a counter.")
|
28
|
+
end
|
29
|
+
metric =
|
30
|
+
if Invoca::Metrics::Prometheus.config_present?
|
31
|
+
prometheus_exporter_config.register_metric(:counter, name, help, { labels: labels, graphite: graphite })
|
32
|
+
end
|
33
|
+
add_metric(Counter.new(name, :counter, source, metric, labels: labels, graphite: graphite))
|
34
|
+
end
|
35
|
+
|
36
|
+
def histogram(name, buckets:, labels: {}, graphite: nil, help: nil)
|
37
|
+
metric =
|
38
|
+
if Invoca::Metrics::Prometheus.config_present?
|
39
|
+
prometheus_exporter_config.register_metric(:histogram, name, help, { labels: labels, graphite: graphite, buckets: buckets })
|
40
|
+
end
|
41
|
+
add_metric(Histogram.new(name, :histogram, source, metric, buckets: buckets, labels: labels, graphite: graphite))
|
42
|
+
end
|
43
|
+
|
44
|
+
def gauge(name, labels: {}, graphite: nil, help: nil)
|
45
|
+
metric =
|
46
|
+
if Invoca::Metrics::Prometheus.config_present?
|
47
|
+
prometheus_exporter_config.register_metric(:gauge, name, help, { labels: labels, graphite: graphite })
|
48
|
+
end
|
49
|
+
add_metric(Gauge.new(name, :gauge, source, metric, labels: labels, graphite: graphite))
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def add_metric(metric)
|
55
|
+
@declared_metrics << metric
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require 'active_support/deprecation'
|
5
|
+
|
6
|
+
module Invoca
|
7
|
+
module Metrics
|
8
|
+
module Prometheus
|
9
|
+
module DeclareMetrics
|
10
|
+
class Gauge < Base
|
11
|
+
|
12
|
+
def set(value = :'1', **labels)
|
13
|
+
if value == :'1'
|
14
|
+
value = 1
|
15
|
+
ActiveSupport::Deprecation.warn("gauge default value of 1 is deprecated; please pass an explicit value")
|
16
|
+
end
|
17
|
+
|
18
|
+
if metric.present?
|
19
|
+
metric.observe(value, default_labels.merge(labels))
|
20
|
+
else
|
21
|
+
warn("Gauge being set without metric being present")
|
22
|
+
end
|
23
|
+
|
24
|
+
if graphite
|
25
|
+
Invoca::Metrics::Client.metrics.gauge(render_graphite_string(**default_labels.merge(labels)), value)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require 'benchmark'
|
5
|
+
require 'active_support/deprecation'
|
6
|
+
|
7
|
+
# TODO: Histogram could override Base#settings to include buckets
|
8
|
+
module Invoca
|
9
|
+
module Metrics
|
10
|
+
module Prometheus
|
11
|
+
module DeclareMetrics
|
12
|
+
class Histogram < Base
|
13
|
+
|
14
|
+
attr_reader :buckets
|
15
|
+
|
16
|
+
def initialize(name, type, source, metric, buckets:, labels:, graphite:)
|
17
|
+
@buckets = buckets
|
18
|
+
super(name, type, source, metric, labels: labels, graphite: graphite)
|
19
|
+
end
|
20
|
+
|
21
|
+
def add(value = :'1', **labels)
|
22
|
+
if value == :'1'
|
23
|
+
value = 1
|
24
|
+
ActiveSupport::Deprecation.warn("histogram default value of 1 is deprecated; please pass an explicit value")
|
25
|
+
end
|
26
|
+
|
27
|
+
if metric.present?
|
28
|
+
metric.observe(value, default_labels.merge(labels)) if metric.present?
|
29
|
+
else
|
30
|
+
warn("Histogram being used without metric being present")
|
31
|
+
end
|
32
|
+
|
33
|
+
if graphite
|
34
|
+
Invoca::Metrics::Client.metrics.timer(render_graphite_string(**default_labels.merge(labels)), value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def time(return_timing: false, **labels)
|
39
|
+
result = nil
|
40
|
+
elapsed_time = Benchmark.realtime { result = yield }
|
41
|
+
elapsed_time_ms = (elapsed_time * 1000).to_i
|
42
|
+
add(elapsed_time_ms, **labels)
|
43
|
+
|
44
|
+
return_timing ? [result, elapsed_time_ms] : result
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Invoca
|
4
|
+
module Metrics
|
5
|
+
module Prometheus
|
6
|
+
class MetricsRegistry
|
7
|
+
class ContradictoryMetricRegistration < StandardError; end
|
8
|
+
|
9
|
+
attr_reader :metrics
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@metrics = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def register(metric)
|
16
|
+
validate_metric_name!(metric)
|
17
|
+
@metrics[metric.name] = metric
|
18
|
+
end
|
19
|
+
|
20
|
+
def method_missing(method_name, *args)
|
21
|
+
metrics[method_name] || super
|
22
|
+
end
|
23
|
+
|
24
|
+
def respond_to?(method_name, *args)
|
25
|
+
@metrics[method_name] || super
|
26
|
+
end
|
27
|
+
|
28
|
+
def respond_to_missing?(method_name, include_private = false)
|
29
|
+
@metrics[method_name] || super
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def validate_metric_name!(metric)
|
35
|
+
if (existing = @metrics[metric.name])
|
36
|
+
metric.settings == existing.settings or
|
37
|
+
raise ContradictoryMetricRegistration, "metric #{metric.name} already registered by #{existing.source} with contradictory settings #{existing.settings} vs new registered #{metric.source} with setting #{metric.settings}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './prometheus/configuration'
|
4
|
+
require_relative './prometheus/metrics_registry'
|
5
|
+
|
6
|
+
module Invoca
|
7
|
+
module Metrics
|
8
|
+
module Prometheus
|
9
|
+
class NotConfiguredError < StandardError; end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# This method is used to configure Invoca::Metrics to export metrics to be
|
13
|
+
# pulled into Prometheus.
|
14
|
+
#
|
15
|
+
# @yield [Invoca::Metrics::Prometheus::Configuration]
|
16
|
+
#
|
17
|
+
# @void
|
18
|
+
def configure
|
19
|
+
@config = Invoca::Metrics::Prometheus::Configuration.new.tap { |config| yield config }.freeze
|
20
|
+
|
21
|
+
# If there are any metrics at this point, it's likely that Prometheus metrics
|
22
|
+
# were declared before invoca-metrics was loaded; walk the set of metrics in the
|
23
|
+
# registry, resetting their PrometheusExporter::Metric objects
|
24
|
+
metrics.metrics.values.each do |metric|
|
25
|
+
case metric.type
|
26
|
+
when :counter
|
27
|
+
metric.reset_metric(@config.register_metric(:counter, metric.name, nil, { labels: metric.default_labels, graphite: metric.graphite }))
|
28
|
+
when :gauge
|
29
|
+
metric.reset_metric(@config.register_metric(:gauge, metric.name, nil, { labels: metric.default_labels, graphite: metric.graphite }))
|
30
|
+
when :histogram
|
31
|
+
metric.reset_metric(@config.register_metric(:histogram, metric.name, nil, { labels: metric.default_labels, graphite: metric.graphite, buckets: metric.buckets }))
|
32
|
+
else
|
33
|
+
warn("Metric type #{metric.type} not one of #{Invoca::Metrics::Prometheus::Configuration::AVAILABLE_METRIC_TYPES}.")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
@config
|
38
|
+
end
|
39
|
+
|
40
|
+
# Accessor for the existing Prometheus configuration
|
41
|
+
#
|
42
|
+
# @raise [Invoca::Metrics::Prometheus::NotConfiguredError]
|
43
|
+
# @return [Invoca::Metrics::Prometheus::Configuration]
|
44
|
+
def config
|
45
|
+
@config or raise NotConfiguredError, 'Invoca::Metrics::Prometheus is trying to be used without being configured'
|
46
|
+
end
|
47
|
+
|
48
|
+
# Helper method for checking if configuration is present
|
49
|
+
#
|
50
|
+
# @return [Boolean]
|
51
|
+
def config_present?
|
52
|
+
@config.present?
|
53
|
+
end
|
54
|
+
|
55
|
+
# TODO: Rename metrics -> registry so that we don't have things like metrics.metrics
|
56
|
+
# (see the configure method above)
|
57
|
+
|
58
|
+
# This method is used to access the global list of all metrics that were registered
|
59
|
+
#
|
60
|
+
# @return [Invoca::Metrics::Prometheus::MetricsRegistry]
|
61
|
+
def metrics
|
62
|
+
@metrics ||= MetricsRegistry.new
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'statsd'
|
4
|
+
|
5
|
+
module Invoca
|
6
|
+
module Metrics
|
7
|
+
class StatsdClient < ::Statsd
|
8
|
+
MILLISECONDS_IN_SECOND = 1000
|
9
|
+
|
10
|
+
@log_send_failures = true
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_accessor :log_send_failures
|
14
|
+
end
|
15
|
+
|
16
|
+
def time(stat, sample_rate = 1)
|
17
|
+
start = Time.now
|
18
|
+
result = yield
|
19
|
+
length_of_time = ((Time.now - start) * MILLISECONDS_IN_SECOND).round
|
20
|
+
timing(stat, length_of_time, sample_rate)
|
21
|
+
[result, length_of_time]
|
22
|
+
end
|
23
|
+
|
24
|
+
def send_to_socket(message)
|
25
|
+
self.class.logger&.debug { "Statsd: #{message}" }
|
26
|
+
socket.send(message, 0)
|
27
|
+
rescue => ex
|
28
|
+
if self.class.log_send_failures
|
29
|
+
self.class.logger&.error { "Statsd exception sending: #{ex.class}: #{ex}" }
|
30
|
+
end
|
31
|
+
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Socket connection should be Thread local and not Fiber local
|
38
|
+
# Can't use `Thread.current[:statsd_client] ||=` because that will be fiber-local as well.
|
39
|
+
def socket
|
40
|
+
Thread.current.thread_variable_get(:statsd_socket) || Thread.current.thread_variable_set(:statsd_socket, new_socket)
|
41
|
+
end
|
42
|
+
|
43
|
+
def new_socket
|
44
|
+
self.class.logger&.info { "Statsd client connection info -- [hostname: #{@host}, port: #{@port}]" }
|
45
|
+
UDPSocket.new.tap { |udp| udp.connect(@host, @port) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext'
|
5
|
+
|
6
|
+
require "invoca/metrics/version"
|
7
|
+
require "invoca/metrics/statsd_client"
|
8
|
+
require "invoca/metrics/client"
|
9
|
+
require "invoca/metrics/direct_metric"
|
10
|
+
require "invoca/metrics/batch"
|
11
|
+
require "invoca/metrics/gauge_cache"
|
12
|
+
require "invoca/metrics/prometheus"
|
13
|
+
require "invoca/metrics/prometheus/declare_metrics/dsl"
|
14
|
+
|
15
|
+
module Invoca
|
16
|
+
module Metrics
|
17
|
+
CONFIG_FIELDS = [:service_name, :server_name, :sub_server_name, :cluster_name, :statsd_host, :statsd_port].freeze
|
18
|
+
|
19
|
+
class << self
|
20
|
+
attr_accessor(*(CONFIG_FIELDS - [:service_name]), :default_config_key)
|
21
|
+
attr_writer :service_name, :graphite_usage_logging_enabled
|
22
|
+
|
23
|
+
def graphite_usage_logging_enabled
|
24
|
+
if @graphite_usage_logging_enabled.nil?
|
25
|
+
@graphite_usage_logging_enabled = true
|
26
|
+
end
|
27
|
+
@graphite_usage_logging_enabled
|
28
|
+
end
|
29
|
+
|
30
|
+
def service_name
|
31
|
+
@service_name or raise ArgumentError, "You must assign a value to Invoca::Metrics.service_name"
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialized?
|
35
|
+
@service_name
|
36
|
+
end
|
37
|
+
|
38
|
+
def config
|
39
|
+
@config ||= {}
|
40
|
+
end
|
41
|
+
|
42
|
+
def config=(config_hash)
|
43
|
+
config_valid?(config_hash) or raise ArgumentError, "Invalid config #{config_hash}. Allowed fields for config key: #{CONFIG_FIELDS}."
|
44
|
+
@config = config_hash
|
45
|
+
end
|
46
|
+
|
47
|
+
def default_client_config
|
48
|
+
{
|
49
|
+
service_name: Invoca::Metrics.service_name,
|
50
|
+
server_name: Invoca::Metrics.server_name,
|
51
|
+
cluster_name: Invoca::Metrics.cluster_name,
|
52
|
+
statsd_host: Invoca::Metrics.statsd_host,
|
53
|
+
statsd_port: Invoca::Metrics.statsd_port,
|
54
|
+
sub_server_name: Invoca::Metrics.sub_server_name
|
55
|
+
}.merge(config[default_config_key] || {})
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def config_valid?(config_hash)
|
61
|
+
config_hash.nil? || config_hash.all? { |_config_key, config_key_hash| (config_key_hash.keys - CONFIG_FIELDS).empty? }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# mix this module into your classes that need to send metrics
|
66
|
+
#
|
67
|
+
module Source
|
68
|
+
extend ActiveSupport::Concern
|
69
|
+
|
70
|
+
module ClassMethods
|
71
|
+
@metrics_namespace = nil
|
72
|
+
|
73
|
+
def metrics_namespace(namespace)
|
74
|
+
@metrics_namespace = namespace
|
75
|
+
end
|
76
|
+
|
77
|
+
def metrics
|
78
|
+
metrics_for(config_key: Invoca::Metrics.default_config_key)
|
79
|
+
end
|
80
|
+
|
81
|
+
def metrics_for(config_key:, namespace: nil)
|
82
|
+
config_from_key = Invoca::Metrics.config[config_key] || {}
|
83
|
+
metrics_config = if (effective_namespace = namespace || @metrics_namespace)
|
84
|
+
config_from_key.merge(namespace: effective_namespace)
|
85
|
+
else
|
86
|
+
config_from_key
|
87
|
+
end
|
88
|
+
Client.metrics(**Invoca::Metrics.default_client_config.merge(metrics_config))
|
89
|
+
end
|
90
|
+
|
91
|
+
def declare_metrics(&block)
|
92
|
+
dsl = Invoca::Metrics::Prometheus::DeclareMetrics::Dsl.new(self)
|
93
|
+
dsl.instance_eval(&block)
|
94
|
+
dsl.declared_metrics.each do |metric|
|
95
|
+
Invoca::Metrics::Prometheus.metrics.register(metric)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def prometheus_metrics
|
100
|
+
Invoca::Metrics::Prometheus.metrics
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def prometheus_metrics
|
105
|
+
self.class.prometheus_metrics
|
106
|
+
end
|
107
|
+
|
108
|
+
def metrics
|
109
|
+
self.class.metrics
|
110
|
+
end
|
111
|
+
|
112
|
+
def metrics_for(config_key:, namespace: nil)
|
113
|
+
self.class.metrics_for(config_key: config_key, namespace: namespace)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
metadata
CHANGED
@@ -1,24 +1,105 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: invoca-metrics
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.14.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Invoca development
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
13
|
-
|
11
|
+
date: 2022-09-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.2'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '7'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '4.2'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '7'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: statsd-ruby
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 1.2.1
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 1.2.1
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: prometheus_exporter
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2.0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: contextual_logger
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '1.0'
|
68
|
+
type: :runtime
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '1.0'
|
75
|
+
description: Invoca metrics reporting library
|
14
76
|
email:
|
15
77
|
- development@invoca.com
|
16
78
|
executables: []
|
17
79
|
extensions: []
|
18
80
|
extra_rdoc_files: []
|
19
|
-
files:
|
20
|
-
|
21
|
-
|
81
|
+
files:
|
82
|
+
- LICENSE.txt
|
83
|
+
- README.md
|
84
|
+
- invoca-metrics.gemspec
|
85
|
+
- lib/invoca/metrics.rb
|
86
|
+
- lib/invoca/metrics/batch.rb
|
87
|
+
- lib/invoca/metrics/client.rb
|
88
|
+
- lib/invoca/metrics/direct_metric.rb
|
89
|
+
- lib/invoca/metrics/gauge_cache.rb
|
90
|
+
- lib/invoca/metrics/prometheus.rb
|
91
|
+
- lib/invoca/metrics/prometheus/configuration.rb
|
92
|
+
- lib/invoca/metrics/prometheus/declare_metrics/base.rb
|
93
|
+
- lib/invoca/metrics/prometheus/declare_metrics/counter.rb
|
94
|
+
- lib/invoca/metrics/prometheus/declare_metrics/dsl.rb
|
95
|
+
- lib/invoca/metrics/prometheus/declare_metrics/gauge.rb
|
96
|
+
- lib/invoca/metrics/prometheus/declare_metrics/histogram.rb
|
97
|
+
- lib/invoca/metrics/prometheus/metrics_registry.rb
|
98
|
+
- lib/invoca/metrics/statsd_client.rb
|
99
|
+
- lib/invoca/metrics/version.rb
|
100
|
+
homepage: https://github.com/Invoca/invoca-metrics
|
101
|
+
licenses:
|
102
|
+
- MIT
|
22
103
|
metadata:
|
23
104
|
allowed_push_host: https://rubygems.org
|
24
105
|
post_install_message:
|
@@ -39,5 +120,5 @@ requirements: []
|
|
39
120
|
rubygems_version: 3.1.6
|
40
121
|
signing_key:
|
41
122
|
specification_version: 4
|
42
|
-
summary: Invoca metrics
|
123
|
+
summary: Invoca metrics reporting library
|
43
124
|
test_files: []
|