gruf-prometheus 1.0.1 → 2.0.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.
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ module Prometheus
20
+ ##
21
+ # Prometheus instrumentor for gRPC servers
22
+ #
23
+ class Collector < Bigcommerce::Prometheus::Collectors::Base
24
+ def collect(metrics = {})
25
+ metrics[:type] = 'grpc'
26
+ rpc_server = grpc_server
27
+ return metrics unless rpc_server
28
+
29
+ rpc_server.instance_variable_get(:@run_mutex).synchronize do
30
+ collect_server_metrics(rpc_server, metrics)
31
+ end
32
+ metrics
33
+ end
34
+
35
+ ##
36
+ # @param [GRPC::RpcServer] rpc_server
37
+ # @param [Hash] metrics
38
+ #
39
+ def collect_server_metrics(rpc_server, metrics)
40
+ pool = rpc_server.instance_variable_get(:@pool)
41
+ metrics[:pool_jobs_waiting_total] = pool.jobs_waiting.to_i
42
+ metrics[:pool_ready_workers_total] = pool.instance_variable_get(:@ready_workers).size
43
+ metrics[:pool_workers_total] = pool.instance_variable_get(:@workers)&.size
44
+ metrics[:pool_initial_size] = rpc_server.instance_variable_get(:@pool_size).to_i
45
+ metrics[:poll_period] = rpc_server.instance_variable_get(:@poll_period).to_i
46
+ end
47
+
48
+ ##
49
+ # @return [GRPC::RpcServer]
50
+ #
51
+ def grpc_server
52
+ @options.fetch(:server, nil)&.server
53
+ end
54
+ end
55
+ end
56
+ end
@@ -24,7 +24,11 @@ module Gruf
24
24
  VALID_CONFIG_KEYS = {
25
25
  process_label: 'grpc',
26
26
  process_name: 'grpc',
27
- collection_frequency: 30
27
+ collection_frequency: 30,
28
+ type_collectors: [],
29
+ collectors: [],
30
+ client_measure_latency: false,
31
+ server_measure_latency: false
28
32
  }.freeze
29
33
 
30
34
  attr_accessor *VALID_CONFIG_KEYS.keys
@@ -39,13 +43,14 @@ module Gruf
39
43
  ##
40
44
  # Yield self for ruby-style initialization
41
45
  #
42
- # @yields [Bigcommerce::Prometheus::Configuration]
43
- # @return [Bigcommerce::Prometheus::Configuration]
46
+ # @yields [Gruf::Prometheus::Configuration]
47
+ # @return [Gruf::Prometheus::Configuration]
44
48
  #
45
49
  def configure
46
50
  reset unless @configured
47
51
  yield self
48
52
  @configured = true
53
+ self
49
54
  end
50
55
 
51
56
  ##
@@ -71,6 +76,8 @@ module Gruf
71
76
  self.process_label = ENV.fetch('PROMETHEUS_PROCESS_LABEL', 'grpc').to_s
72
77
  self.process_name = ENV.fetch('PROMETHEUS_PROCESS_NAME', 'grpc').to_s
73
78
  self.collection_frequency = ENV.fetch('PROMETHEUS_COLLECTION_FREQUENCY', 30).to_i
79
+ self.server_measure_latency = ENV.fetch('PROMETHEUS_SERVER_MEASURE_LATENCY', 0).to_i.positive?
80
+ self.client_measure_latency = ENV.fetch('PROMETHEUS_CLIENT_MEASURE_LATENCY', 0).to_i.positive?
74
81
  end
75
82
 
76
83
  ##
@@ -28,8 +28,13 @@ module Gruf
28
28
  #
29
29
  def before_server_start(server:)
30
30
  logger.info "[gruf-prometheus][#{::Gruf::Prometheus.process_name}] Starting #{server.class}"
31
- prometheus_server.add_type_collector(::Gruf::Prometheus::TypeCollectors::Grpc.new)
31
+ prometheus_server.add_type_collector(::Gruf::Prometheus::TypeCollector.new)
32
+ prometheus_server.add_type_collector(::Gruf::Prometheus::Server::TypeCollector.new)
33
+ prometheus_server.add_type_collector(::Gruf::Prometheus::Client::TypeCollector.new)
32
34
  prometheus_server.add_type_collector(::PrometheusExporter::Server::ActiveRecordCollector.new)
35
+ custom_type_collectors.each do |tc|
36
+ prometheus_server.add_type_collector(tc)
37
+ end
33
38
  prometheus_server.start
34
39
  sleep 2 unless ENV['RACK_ENV'] == 'test' # wait for server to come online
35
40
  start_collectors(server: server)
@@ -58,11 +63,17 @@ module Gruf
58
63
  client: ::Bigcommerce::Prometheus.client,
59
64
  frequency: ::Gruf::Prometheus.collection_frequency
60
65
  )
61
- ::Gruf::Prometheus::Collectors::Grpc.start(
62
- server: server,
66
+ ::Gruf::Prometheus::Collector.start(
67
+ options: {
68
+ server: server
69
+ },
70
+ type: 'grpc',
63
71
  client: ::Bigcommerce::Prometheus.client,
64
72
  frequency: ::Gruf::Prometheus.collection_frequency
65
73
  )
74
+ custom_collectors.each do |collector, arguments|
75
+ collector.start(arguments)
76
+ end
66
77
  end
67
78
 
68
79
  ##
@@ -70,11 +81,11 @@ module Gruf
70
81
  #
71
82
  def stop_collectors
72
83
  ::PrometheusExporter::Instrumentation::Process.stop
73
- ::Gruf::Prometheus::Collectors::Grpc.stop
84
+ ::Gruf::Prometheus::Collector.stop
74
85
  end
75
86
 
76
87
  ##
77
- # @return [Gruf::Prometheus::Server]
88
+ # @return [Bigcommerce::Prometheus::Server]
78
89
  #
79
90
  def prometheus_server
80
91
  @prometheus_server ||= ::Bigcommerce::Prometheus::Server.new(
@@ -85,6 +96,20 @@ module Gruf
85
96
  logger: logger
86
97
  )
87
98
  end
99
+
100
+ ##
101
+ # @return [Array<Bigcommerce::Prometheus::TypeCollectors::Base>]
102
+ #
103
+ def custom_type_collectors
104
+ @options.fetch(:type_collectors, []) || []
105
+ end
106
+
107
+ ##
108
+ # @return [Array<Bigcommerce::Prometheus::Collectors::Base>]
109
+ #
110
+ def custom_collectors
111
+ @options.fetch(:collectors, []) || []
112
+ end
88
113
  end
89
114
  end
90
115
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ module Prometheus
20
+ module RequestTypes
21
+ UNARY = 'UNARY'
22
+ CLIENT_STREAM = 'CLIENT_STREAM'
23
+ SERVER_STREAM = 'SERVER_STREAM'
24
+ BIDI_STREAM = 'BIDI_STREAM'
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ module Prometheus
20
+ module Server
21
+ ##
22
+ # Prometheus instrumentor for gRPC servers
23
+ #
24
+ class Collector < Bigcommerce::Prometheus::Collectors::Base
25
+ RESPONSE_CODE_OK = 'OK'
26
+
27
+ ##
28
+ # @param [Gruf::Controller::Request] request
29
+ #
30
+ def started_total(request:)
31
+ push(
32
+ grpc_server_started_total: 1,
33
+ custom_labels: custom_labels(request: request)
34
+ )
35
+ end
36
+
37
+ ##
38
+ # @param [Gruf::Controller::Request] request
39
+ # @param [Gruf::Interceptors::Timer::Result] result:party
40
+ #
41
+ def handled_total(request:, result:)
42
+ push(
43
+ grpc_server_handled_total: 1,
44
+ custom_labels: custom_labels(request: request, result: result)
45
+ )
46
+ end
47
+
48
+ ##
49
+ # @param [Gruf::Controller::Request] request
50
+ # @param [Gruf::Interceptors::Timer::Result] result
51
+ #
52
+ def handled_latency_seconds(request:, result:)
53
+ push(
54
+ grpc_server_handled_latency_seconds: result.elapsed.to_f,
55
+ custom_labels: custom_labels(request: request, result: result)
56
+ )
57
+ end
58
+
59
+ private
60
+
61
+ ##
62
+ # @param [Gruf::Controller::Request] request
63
+ # @param [Gruf::Interceptors::Timer::Result|NilClass] result
64
+ # @return [Hash]
65
+ #
66
+ def custom_labels(request:, result: nil)
67
+ labels = {
68
+ grpc_service: format_grpc_service_name(request.service.name.to_s),
69
+ grpc_method: format_grpc_method_name(request.method_key.to_s),
70
+ grpc_type: determine_type(request)
71
+ }
72
+ if result
73
+ labels[:grpc_code] = result.successful? ? RESPONSE_CODE_OK : result.message_class_name.split('::').last
74
+ end
75
+ labels
76
+ end
77
+
78
+ ##
79
+ # Format the service name as `path.to.Service` (from Path::To::Service)
80
+ #
81
+ # @param [String] name
82
+ # @return [String]
83
+ #
84
+ def format_grpc_service_name(name)
85
+ parts = name.split('::')
86
+ return '' unless parts.any?
87
+
88
+ svc = parts.pop.to_s
89
+ parts.map!(&:downcase)
90
+ parts << svc
91
+ parts.join('.')
92
+ end
93
+
94
+ ##
95
+ # Format the method name as `MethodName` (from method_name)
96
+ #
97
+ # @param [String] name
98
+ # @return [String]
99
+ #
100
+ def format_grpc_method_name(name)
101
+ name.split('_').map(&:capitalize).join
102
+ end
103
+
104
+ ##
105
+ # @param [Gruf::Controller::Request] request
106
+ # @return [String]
107
+ #
108
+ def determine_type(request)
109
+ if request.client_streamer?
110
+ Gruf::Prometheus::RequestTypes::CLIENT_STREAM
111
+ elsif request.server_streamer?
112
+ Gruf::Prometheus::RequestTypes::SERVER_STREAM
113
+ elsif request.bidi_streamer?
114
+ Gruf::Prometheus::RequestTypes::BIDI_STREAM
115
+ else
116
+ Gruf::Prometheus::RequestTypes::UNARY
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ module Prometheus
20
+ module Server
21
+ ##
22
+ # Server interceptor for measuring counter/timers for gRPC inbound requests
23
+ #
24
+ class Interceptor < ::Gruf::Interceptors::ServerInterceptor
25
+ ##
26
+ # Intercept the call and send metrics
27
+ #
28
+ def call(&block)
29
+ result = ::Gruf::Interceptors::Timer.time(&block)
30
+
31
+ send_metrics(result)
32
+
33
+ raise result.message unless result.successful?
34
+
35
+ result.message
36
+ end
37
+
38
+ private
39
+
40
+ ##
41
+ # @param [Gruf::Interceptors::Timer::Result] result
42
+ #
43
+ def send_metrics(result)
44
+ prometheus_collector.started_total(request: request)
45
+ prometheus_collector.handled_total(request: request, result: result)
46
+ prometheus_collector.handled_latency_seconds(request: request, result: result) if measure_latency?
47
+ rescue StandardError => e
48
+ # we don't want this to affect actual RPC execution, so just log an error and move on
49
+ Gruf.logger.error "Failed registering metric to prometheus type collector: #{e.message} - #{e.class.name}"
50
+ end
51
+
52
+ ##
53
+ # @return [::Gruf::Prometheus::Server::Collector]
54
+ #
55
+ def prometheus_collector
56
+ @prometheus_collector ||= ::Gruf::Prometheus::Server::Collector.new(type: 'grpc_server')
57
+ end
58
+
59
+ ##
60
+ # @return [Boolean]
61
+ #
62
+ def measure_latency?
63
+ unless @measure_latency
64
+ v = @options.fetch(:measure_latency, ::Gruf::Prometheus.server_measure_latency)
65
+ @measure_latency = v.nil? ? false : v
66
+ end
67
+ @measure_latency
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019-present, BigCommerce Pty. Ltd. All rights reserved
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ module Gruf
19
+ module Prometheus
20
+ module Server
21
+ ##
22
+ # Type Collector for grpc server metrics
23
+ #
24
+ class TypeCollector < Bigcommerce::Prometheus::TypeCollectors::Base
25
+ def type
26
+ 'grpc_server'
27
+ end
28
+
29
+ private
30
+
31
+ ##
32
+ # Initialize the collector
33
+ #
34
+ def build_metrics
35
+ metrics = {
36
+ grpc_server_started_total: PrometheusExporter::Metric::Counter.new('grpc_server_started_total', 'Total number of RPCs started on the server'),
37
+ grpc_server_handled_total: PrometheusExporter::Metric::Counter.new('grpc_server_handled_total', 'Total number of RPCs completed on the server, regardless of success or failure')
38
+ }
39
+ metrics[:grpc_server_handled_latency_seconds] = PrometheusExporter::Metric::Histogram.new('grpc_server_handled_latency_seconds', 'Histogram of response latency of RPCs handled by the server, in seconds') if measure_latency?
40
+ metrics
41
+ end
42
+
43
+ ##
44
+ # Collect the object into the buffer
45
+ #
46
+ def collect_metrics(data: {}, labels: {})
47
+ metric(:grpc_server_started_total)&.observe(data['grpc_server_started_total'].to_i, labels)
48
+ metric(:grpc_server_handled_total)&.observe(data['grpc_server_handled_total'].to_i, labels)
49
+ metric(:grpc_server_handled_latency_seconds)&.observe(data['grpc_server_handled_latency_seconds'].to_f, labels) if measure_latency?
50
+ end
51
+
52
+ ##
53
+ # @return [Boolean]
54
+ #
55
+ def measure_latency?
56
+ @measure_latency ||= ::Gruf::Prometheus.server_measure_latency
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end