metrics.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. /*
  2. Copyright © The ESO Authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package grpc
  14. import (
  15. "errors"
  16. "fmt"
  17. "strconv"
  18. "time"
  19. "github.com/prometheus/client_golang/prometheus"
  20. )
  21. var (
  22. // gRPC latency buckets optimized for typical RPC call durations.
  23. grpcLatencyBuckets = []float64{0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10, 30}
  24. // Connection Pool gauges.
  25. poolConnectionsActive = prometheus.NewGaugeVec(
  26. prometheus.GaugeOpts{
  27. Name: "grpc_pool_connections_active",
  28. Help: "Number of active gRPC connections in the pool with references > 0",
  29. },
  30. []string{"address", "tls_enabled"},
  31. )
  32. poolConnectionsIdle = prometheus.NewGaugeVec(
  33. prometheus.GaugeOpts{
  34. Name: "grpc_pool_connections_idle",
  35. Help: "Number of idle gRPC connections in the pool with references = 0",
  36. },
  37. []string{"address", "tls_enabled"},
  38. )
  39. poolConnectionsTotal = prometheus.NewGaugeVec(
  40. prometheus.GaugeOpts{
  41. Name: "grpc_pool_connections_total",
  42. Help: "Total number of gRPC connections in the pool",
  43. },
  44. []string{"address", "tls_enabled"},
  45. )
  46. // Connection Pool histograms.
  47. connectionAge = prometheus.NewHistogramVec(
  48. prometheus.HistogramOpts{
  49. Name: "grpc_connection_age_seconds",
  50. Help: "Age of gRPC connections in seconds",
  51. Buckets: []float64{60, 300, 600, 900, 1800, 3600}, // 1m, 5m, 10m, 15m, 30m, 1h
  52. },
  53. []string{"address", "tls_enabled"},
  54. )
  55. connectionIdle = prometheus.NewHistogramVec(
  56. prometheus.HistogramOpts{
  57. Name: "grpc_connection_idle_seconds",
  58. Help: "Idle time of gRPC connections in seconds",
  59. Buckets: []float64{30, 60, 120, 300, 600, 900}, // 30s, 1m, 2m, 5m, 10m, 15m
  60. },
  61. []string{"address", "tls_enabled"},
  62. )
  63. // Connection Pool counters.
  64. poolHits = prometheus.NewCounterVec(
  65. prometheus.CounterOpts{
  66. Name: "grpc_pool_hits_total",
  67. Help: "Total number of connection pool cache hits (connection reused)",
  68. },
  69. []string{"address", "tls_enabled"},
  70. )
  71. poolMisses = prometheus.NewCounterVec(
  72. prometheus.CounterOpts{
  73. Name: "grpc_pool_misses_total",
  74. Help: "Total number of connection pool cache misses (new connection created)",
  75. },
  76. []string{"address", "tls_enabled"},
  77. )
  78. poolEvictions = prometheus.NewCounterVec(
  79. prometheus.CounterOpts{
  80. Name: "grpc_pool_evictions_total",
  81. Help: "Total number of connection pool evictions",
  82. },
  83. []string{"address", "tls_enabled", "eviction_reason"},
  84. )
  85. poolConnectionErrors = prometheus.NewCounterVec(
  86. prometheus.CounterOpts{
  87. Name: "grpc_pool_connection_errors_total",
  88. Help: "Total number of failed connection attempts",
  89. },
  90. []string{"address", "tls_enabled"},
  91. )
  92. // gRPC client metrics.
  93. clientRequestDuration = prometheus.NewHistogramVec(
  94. prometheus.HistogramOpts{
  95. Name: "grpc_client_request_duration_seconds",
  96. Help: "Duration of gRPC client requests in seconds",
  97. Buckets: grpcLatencyBuckets,
  98. },
  99. []string{"method", "target", "status"},
  100. )
  101. clientRequestsTotal = prometheus.NewCounterVec(
  102. prometheus.CounterOpts{
  103. Name: "grpc_client_requests_total",
  104. Help: "Total number of gRPC client requests",
  105. },
  106. []string{"method", "target", "status"},
  107. )
  108. clientRequestErrors = prometheus.NewCounterVec(
  109. prometheus.CounterOpts{
  110. Name: "grpc_client_request_errors_total",
  111. Help: "Total number of failed gRPC client requests",
  112. },
  113. []string{"method", "target", "error_type"},
  114. )
  115. )
  116. // PoolMetrics records connection-pool metrics.
  117. type PoolMetrics interface {
  118. RecordHit(address string, tlsEnabled bool)
  119. RecordMiss(address string, tlsEnabled bool)
  120. RecordEviction(address string, tlsEnabled bool, reason string)
  121. RecordConnectionError(address string, tlsEnabled bool)
  122. UpdatePoolState(address string, tlsEnabled bool, active, idle, total int)
  123. RecordConnectionAge(address string, tlsEnabled bool, age time.Duration)
  124. RecordConnectionIdle(address string, tlsEnabled bool, idle time.Duration)
  125. }
  126. // RequestObserver records client request metrics.
  127. type RequestObserver interface {
  128. ObserveRequest(method, target string, err error, duration time.Duration)
  129. }
  130. // defaultPoolMetrics implements PoolMetrics using Prometheus.
  131. type defaultPoolMetrics struct{}
  132. // RecordHit records a connection pool cache hit.
  133. func (m *defaultPoolMetrics) RecordHit(address string, tlsEnabled bool) {
  134. poolHits.WithLabelValues(address, strconv.FormatBool(tlsEnabled)).Inc()
  135. }
  136. // RecordMiss records a connection pool cache miss.
  137. func (m *defaultPoolMetrics) RecordMiss(address string, tlsEnabled bool) {
  138. poolMisses.WithLabelValues(address, strconv.FormatBool(tlsEnabled)).Inc()
  139. }
  140. // RecordEviction records a connection eviction with reason.
  141. func (m *defaultPoolMetrics) RecordEviction(address string, tlsEnabled bool, reason string) {
  142. poolEvictions.WithLabelValues(address, strconv.FormatBool(tlsEnabled), reason).Inc()
  143. }
  144. // RecordConnectionError records a failed connection attempt.
  145. func (m *defaultPoolMetrics) RecordConnectionError(address string, tlsEnabled bool) {
  146. poolConnectionErrors.WithLabelValues(address, strconv.FormatBool(tlsEnabled)).Inc()
  147. }
  148. // UpdatePoolState updates the current pool state gauges.
  149. func (m *defaultPoolMetrics) UpdatePoolState(address string, tlsEnabled bool, active, idle, total int) {
  150. labels := prometheus.Labels{"address": address, "tls_enabled": strconv.FormatBool(tlsEnabled)}
  151. poolConnectionsActive.With(labels).Set(float64(active))
  152. poolConnectionsIdle.With(labels).Set(float64(idle))
  153. poolConnectionsTotal.With(labels).Set(float64(total))
  154. }
  155. // RecordConnectionAge records the age of a connection.
  156. func (m *defaultPoolMetrics) RecordConnectionAge(address string, tlsEnabled bool, age time.Duration) {
  157. connectionAge.WithLabelValues(address, strconv.FormatBool(tlsEnabled)).Observe(age.Seconds())
  158. }
  159. // RecordConnectionIdle records the idle time of a connection.
  160. func (m *defaultPoolMetrics) RecordConnectionIdle(address string, tlsEnabled bool, idle time.Duration) {
  161. connectionIdle.WithLabelValues(address, strconv.FormatBool(tlsEnabled)).Observe(idle.Seconds())
  162. }
  163. // defaultClientMetrics implements RequestObserver using Prometheus.
  164. type defaultClientMetrics struct{}
  165. // ObserveRequest records metrics for a client request.
  166. func (m *defaultClientMetrics) ObserveRequest(method, target string, err error, duration time.Duration) {
  167. status := "success"
  168. if err != nil {
  169. status = "error"
  170. errorType := classifyError(err)
  171. clientRequestErrors.WithLabelValues(method, target, errorType).Inc()
  172. }
  173. clientRequestDuration.WithLabelValues(method, target, status).Observe(duration.Seconds())
  174. clientRequestsTotal.WithLabelValues(method, target, status).Inc()
  175. }
  176. // classifyError extracts error type for metrics.
  177. func classifyError(err error) string {
  178. if err == nil {
  179. return "none"
  180. }
  181. errStr := err.Error()
  182. // Classify common error patterns
  183. switch {
  184. case contains(errStr, "context deadline exceeded"):
  185. return "timeout"
  186. case contains(errStr, "connection refused"):
  187. return "connection_refused"
  188. case contains(errStr, "unavailable"):
  189. return "unavailable"
  190. case contains(errStr, "forbidden"):
  191. return "forbidden"
  192. case contains(errStr, "not found"):
  193. return "not_found"
  194. case contains(errStr, "unauthorized"):
  195. return "unauthorized"
  196. default:
  197. return "unknown"
  198. }
  199. }
  200. func contains(s, substr string) bool {
  201. return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || findSubstring(s, substr)))
  202. }
  203. func findSubstring(s, substr string) bool {
  204. for i := 0; i <= len(s)-len(substr); i++ {
  205. if s[i:i+len(substr)] == substr {
  206. return true
  207. }
  208. }
  209. return false
  210. }
  211. // Global instances.
  212. var (
  213. poolMetrics PoolMetrics = &defaultPoolMetrics{}
  214. clientMetrics RequestObserver = &defaultClientMetrics{}
  215. )
  216. // RegisterMetrics registers all gRPC metrics with Prometheus.
  217. func RegisterMetrics(registry prometheus.Registerer) error {
  218. collectors := []prometheus.Collector{
  219. poolConnectionsActive,
  220. poolConnectionsIdle,
  221. poolConnectionsTotal,
  222. connectionAge,
  223. connectionIdle,
  224. poolHits,
  225. poolMisses,
  226. poolEvictions,
  227. poolConnectionErrors,
  228. clientRequestDuration,
  229. clientRequestsTotal,
  230. clientRequestErrors,
  231. }
  232. for _, collector := range collectors {
  233. if err := registry.Register(collector); err != nil {
  234. // Check if already registered.
  235. var alreadyRegistered prometheus.AlreadyRegisteredError
  236. if errors.As(err, &alreadyRegistered) {
  237. continue
  238. }
  239. return fmt.Errorf("failed to register metric: %w", err)
  240. }
  241. }
  242. return nil
  243. }
  244. // GetPoolMetrics returns the pool metrics instance for testing.
  245. func GetPoolMetrics() PoolMetrics {
  246. return poolMetrics
  247. }
  248. // GetClientMetrics returns the client metrics instance for testing.
  249. func GetClientMetrics() RequestObserver {
  250. return clientMetrics
  251. }