gruf-prometheus 1.0.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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