metrics.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. package frankenphp
  2. import (
  3. "github.com/dunglas/frankenphp/internal/fastabs"
  4. "regexp"
  5. "sync"
  6. "time"
  7. "github.com/prometheus/client_golang/prometheus"
  8. )
  9. var metricsNameRegex = regexp.MustCompile(`\W+`)
  10. var metricsNameFixRegex = regexp.MustCompile(`^_+|_+$`)
  11. var VersionString = "dev"
  12. const (
  13. StopReasonCrash = iota
  14. StopReasonRestart
  15. StopReasonShutdown
  16. )
  17. type StopReason int
  18. type Metrics interface {
  19. // StartWorker collects started workers
  20. StartWorker(name string)
  21. // ReadyWorker collects ready workers
  22. ReadyWorker(name string)
  23. // StopWorker collects stopped workers
  24. StopWorker(name string, reason StopReason)
  25. // TotalWorkers collects expected workers
  26. TotalWorkers(name string, num int)
  27. // TotalThreads collects total threads
  28. TotalThreads(num int)
  29. // StartRequest collects started requests
  30. StartRequest()
  31. // StopRequest collects stopped requests
  32. StopRequest()
  33. // StopWorkerRequest collects stopped worker requests
  34. StopWorkerRequest(name string, duration time.Duration)
  35. // StartWorkerRequest collects started worker requests
  36. StartWorkerRequest(name string)
  37. Shutdown()
  38. }
  39. type nullMetrics struct{}
  40. func (n nullMetrics) StartWorker(string) {
  41. }
  42. func (n nullMetrics) ReadyWorker(string) {
  43. }
  44. func (n nullMetrics) StopWorker(string, StopReason) {
  45. }
  46. func (n nullMetrics) TotalWorkers(string, int) {
  47. }
  48. func (n nullMetrics) TotalThreads(int) {
  49. }
  50. func (n nullMetrics) StartRequest() {
  51. }
  52. func (n nullMetrics) StopRequest() {
  53. }
  54. func (n nullMetrics) StopWorkerRequest(string, time.Duration) {
  55. }
  56. func (n nullMetrics) StartWorkerRequest(string) {
  57. }
  58. func (n nullMetrics) Shutdown() {
  59. }
  60. type PrometheusMetrics struct {
  61. registry prometheus.Registerer
  62. totalThreads prometheus.Counter
  63. busyThreads prometheus.Gauge
  64. totalWorkers map[string]prometheus.Gauge
  65. busyWorkers map[string]prometheus.Gauge
  66. readyWorkers map[string]prometheus.Gauge
  67. workerCrashes map[string]prometheus.Counter
  68. workerRestarts map[string]prometheus.Counter
  69. workerRequestTime map[string]prometheus.Counter
  70. workerRequestCount map[string]prometheus.Counter
  71. version *prometheus.GaugeVec
  72. mu sync.Mutex
  73. }
  74. func (m *PrometheusMetrics) StartWorker(name string) {
  75. m.busyThreads.Inc()
  76. // tests do not register workers before starting them
  77. if _, ok := m.totalWorkers[name]; !ok {
  78. return
  79. }
  80. m.totalWorkers[name].Inc()
  81. }
  82. func (m *PrometheusMetrics) ReadyWorker(name string) {
  83. if _, ok := m.totalWorkers[name]; !ok {
  84. return
  85. }
  86. m.readyWorkers[name].Inc()
  87. }
  88. func (m *PrometheusMetrics) StopWorker(name string, reason StopReason) {
  89. m.busyThreads.Dec()
  90. // tests do not register workers before starting them
  91. if _, ok := m.totalWorkers[name]; !ok {
  92. return
  93. }
  94. m.totalWorkers[name].Dec()
  95. m.readyWorkers[name].Dec()
  96. if reason == StopReasonCrash {
  97. m.workerCrashes[name].Inc()
  98. } else if reason == StopReasonRestart {
  99. m.workerRestarts[name].Inc()
  100. } else if reason == StopReasonShutdown {
  101. m.totalWorkers[name].Dec()
  102. }
  103. }
  104. func (m *PrometheusMetrics) getIdentity(name string) (string, error) {
  105. actualName, err := fastabs.FastAbs(name)
  106. if err != nil {
  107. return name, err
  108. }
  109. return actualName, nil
  110. }
  111. func (m *PrometheusMetrics) TotalWorkers(name string, _ int) {
  112. m.mu.Lock()
  113. defer m.mu.Unlock()
  114. identity, err := m.getIdentity(name)
  115. if err != nil {
  116. // do not create metrics, let error propagate when worker is started
  117. return
  118. }
  119. subsystem := getWorkerNameForMetrics(name)
  120. if _, ok := m.totalWorkers[identity]; !ok {
  121. m.totalWorkers[identity] = prometheus.NewGauge(prometheus.GaugeOpts{
  122. Namespace: "frankenphp",
  123. Subsystem: subsystem,
  124. Name: "total_workers",
  125. Help: "Total number of PHP workers for this worker",
  126. })
  127. m.registry.MustRegister(m.totalWorkers[identity])
  128. }
  129. if _, ok := m.workerCrashes[identity]; !ok {
  130. m.workerCrashes[identity] = prometheus.NewCounter(prometheus.CounterOpts{
  131. Namespace: "frankenphp",
  132. Subsystem: subsystem,
  133. Name: "worker_crashes",
  134. Help: "Number of PHP worker crashes for this worker",
  135. })
  136. m.registry.MustRegister(m.workerCrashes[identity])
  137. }
  138. if _, ok := m.workerRestarts[identity]; !ok {
  139. m.workerRestarts[identity] = prometheus.NewCounter(prometheus.CounterOpts{
  140. Namespace: "frankenphp",
  141. Subsystem: subsystem,
  142. Name: "worker_restarts",
  143. Help: "Number of PHP worker restarts for this worker",
  144. })
  145. m.registry.MustRegister(m.workerRestarts[identity])
  146. }
  147. if _, ok := m.readyWorkers[identity]; !ok {
  148. m.readyWorkers[identity] = prometheus.NewGauge(prometheus.GaugeOpts{
  149. Namespace: "frankenphp",
  150. Subsystem: subsystem,
  151. Name: "ready_workers",
  152. Help: "Running workers that have successfully called frankenphp_handle_request at least once",
  153. })
  154. m.registry.MustRegister(m.readyWorkers[identity])
  155. }
  156. if _, ok := m.busyWorkers[identity]; !ok {
  157. m.busyWorkers[identity] = prometheus.NewGauge(prometheus.GaugeOpts{
  158. Namespace: "frankenphp",
  159. Subsystem: subsystem,
  160. Name: "busy_workers",
  161. Help: "Number of busy PHP workers for this worker",
  162. })
  163. m.registry.MustRegister(m.busyWorkers[identity])
  164. }
  165. if _, ok := m.workerRequestTime[identity]; !ok {
  166. m.workerRequestTime[identity] = prometheus.NewCounter(prometheus.CounterOpts{
  167. Namespace: "frankenphp",
  168. Subsystem: subsystem,
  169. Name: "worker_request_time",
  170. })
  171. m.registry.MustRegister(m.workerRequestTime[identity])
  172. }
  173. if _, ok := m.workerRequestCount[identity]; !ok {
  174. m.workerRequestCount[identity] = prometheus.NewCounter(prometheus.CounterOpts{
  175. Namespace: "frankenphp",
  176. Subsystem: subsystem,
  177. Name: "worker_request_count",
  178. })
  179. m.registry.MustRegister(m.workerRequestCount[identity])
  180. }
  181. }
  182. func (m *PrometheusMetrics) TotalThreads(num int) {
  183. m.totalThreads.Add(float64(num))
  184. }
  185. func (m *PrometheusMetrics) StartRequest() {
  186. m.busyThreads.Inc()
  187. }
  188. func (m *PrometheusMetrics) StopRequest() {
  189. m.busyThreads.Dec()
  190. }
  191. func (m *PrometheusMetrics) StopWorkerRequest(name string, duration time.Duration) {
  192. if _, ok := m.workerRequestTime[name]; !ok {
  193. return
  194. }
  195. m.workerRequestCount[name].Inc()
  196. m.busyWorkers[name].Dec()
  197. m.workerRequestTime[name].Add(duration.Seconds())
  198. }
  199. func (m *PrometheusMetrics) StartWorkerRequest(name string) {
  200. if _, ok := m.busyWorkers[name]; !ok {
  201. return
  202. }
  203. m.busyWorkers[name].Inc()
  204. }
  205. func (m *PrometheusMetrics) Shutdown() {
  206. m.registry.Unregister(m.totalThreads)
  207. m.registry.Unregister(m.busyThreads)
  208. for _, g := range m.totalWorkers {
  209. m.registry.Unregister(g)
  210. }
  211. for _, g := range m.busyWorkers {
  212. m.registry.Unregister(g)
  213. }
  214. for _, c := range m.workerRequestTime {
  215. m.registry.Unregister(c)
  216. }
  217. for _, c := range m.workerRequestCount {
  218. m.registry.Unregister(c)
  219. }
  220. for _, c := range m.workerCrashes {
  221. m.registry.Unregister(c)
  222. }
  223. for _, c := range m.workerRestarts {
  224. m.registry.Unregister(c)
  225. }
  226. for _, g := range m.readyWorkers {
  227. m.registry.Unregister(g)
  228. }
  229. m.totalThreads = prometheus.NewCounter(prometheus.CounterOpts{
  230. Name: "frankenphp_total_threads",
  231. Help: "Total number of PHP threads",
  232. })
  233. m.busyThreads = prometheus.NewGauge(prometheus.GaugeOpts{
  234. Name: "frankenphp_busy_threads",
  235. Help: "Number of busy PHP threads",
  236. })
  237. m.totalWorkers = map[string]prometheus.Gauge{}
  238. m.busyWorkers = map[string]prometheus.Gauge{}
  239. m.workerRequestTime = map[string]prometheus.Counter{}
  240. m.workerRequestCount = map[string]prometheus.Counter{}
  241. m.workerRestarts = map[string]prometheus.Counter{}
  242. m.workerCrashes = map[string]prometheus.Counter{}
  243. m.readyWorkers = map[string]prometheus.Gauge{}
  244. // version does not change while running/restarting/stopping
  245. m.registry.MustRegister(m.totalThreads)
  246. m.registry.MustRegister(m.busyThreads)
  247. }
  248. func getWorkerNameForMetrics(name string) string {
  249. name = metricsNameRegex.ReplaceAllString(name, "_")
  250. name = metricsNameFixRegex.ReplaceAllString(name, "")
  251. return name
  252. }
  253. func NewPrometheusMetrics(registry prometheus.Registerer) *PrometheusMetrics {
  254. if registry == nil {
  255. registry = prometheus.NewRegistry()
  256. }
  257. m := &PrometheusMetrics{
  258. registry: registry,
  259. totalThreads: prometheus.NewCounter(prometheus.CounterOpts{
  260. Name: "frankenphp_total_threads",
  261. Help: "Total number of PHP threads",
  262. }),
  263. busyThreads: prometheus.NewGauge(prometheus.GaugeOpts{
  264. Name: "frankenphp_busy_threads",
  265. Help: "Number of busy PHP threads",
  266. }),
  267. totalWorkers: map[string]prometheus.Gauge{},
  268. busyWorkers: map[string]prometheus.Gauge{},
  269. workerRequestTime: map[string]prometheus.Counter{},
  270. workerRequestCount: map[string]prometheus.Counter{},
  271. workerRestarts: map[string]prometheus.Counter{},
  272. workerCrashes: map[string]prometheus.Counter{},
  273. readyWorkers: map[string]prometheus.Gauge{},
  274. version: prometheus.NewGaugeVec(prometheus.GaugeOpts{
  275. Name: "frankenphp_version",
  276. Help: "Current version of FrankenPHP",
  277. }, []string{"version", "php_version"}),
  278. }
  279. m.registry.MustRegister(m.version)
  280. m.version.WithLabelValues(VersionString, Version().Version).Set(1)
  281. m.registry.MustRegister(m.totalThreads)
  282. m.registry.MustRegister(m.busyThreads)
  283. return m
  284. }