penpot/backend/src/app/metrics.clj
Andrey Antukh d380efdb0c
⬆️ Update devenv dependencies (#9142)
* ⬆️ Update devenv dependencies

*  Fix formatting issues

* 📎 Fix linter issues
2026-04-24 14:07:51 +02:00

242 lines
7.5 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.metrics
(:refer-clojure :exclude [run!])
(:require
[app.common.logging :as l]
[app.common.schema :as sm]
[app.metrics.definition :as-alias mdef]
[integrant.core :as ig])
(:import
io.prometheus.client.CollectorRegistry
io.prometheus.client.Counter
io.prometheus.client.Counter$Child
io.prometheus.client.exporter.common.TextFormat
io.prometheus.client.Gauge
io.prometheus.client.Gauge$Child
io.prometheus.client.Histogram
io.prometheus.client.Histogram$Child
io.prometheus.client.hotspot.DefaultExports
io.prometheus.client.SimpleCollector
io.prometheus.client.Summary
io.prometheus.client.Summary$Builder
io.prometheus.client.Summary$Child
java.io.StringWriter))
(set! *warn-on-reflection* true)
(declare create-registry)
(declare create-collector)
(declare handler)
(defprotocol IMetrics
(get-registry [_])
(get-collector [_ id])
(get-handler [_]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; METRICS SERVICE PROVIDER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(sm/register!
{:type ::collector
:pred #(instance? SimpleCollector %)
:type-properties
{:title "collector"
:description "An instance of SimpleCollector"}})
(sm/register!
{:type ::registry
:pred #(instance? CollectorRegistry %)
:type-properties
{:title "Metrics Registry"
:description "Instance of CollectorRegistry"}})
(def ^:private schema:definitions
[:map-of :keyword
[:map {:title "definition"}
[::mdef/name :string]
[::mdef/help :string]
[::mdef/type [:enum :gauge :counter :summary :histogram]]
[::mdef/labels {:optional true} [::sm/vec :string]]
[::mdef/instance {:optional true} ::collector]]])
(defn metrics?
[o]
(satisfies? IMetrics o))
(sm/register!
{:type ::metrics
:pred metrics?})
(def ^:private valid-definitions?
(sm/validator schema:definitions))
(defmethod ig/assert-key ::metrics
[_ {:keys [default]}]
(assert (valid-definitions? default) "expected valid definitions"))
(defmethod ig/init-key ::metrics
[_ cfg]
(l/info :action "initialize metrics")
(let [registry (create-registry)
definitions (reduce-kv (fn [res k v]
(->> (assoc v ::registry registry)
(create-collector)
(assoc res k)))
{}
(:default cfg))]
(reify
IMetrics
(get-handler [_]
(partial handler registry))
(get-collector [_ id]
(get definitions id))
(get-registry [_]
registry))))
(defn- handler
[registry _]
(let [samples (.metricFamilySamples ^CollectorRegistry registry)
writer (StringWriter.)]
(TextFormat/write004 writer samples)
{:headers {"content-type" TextFormat/CONTENT_TYPE_004}
:body (.toString writer)}))
(defmethod ig/assert-key ::routes
[_ {:keys [::metrics]}]
(assert (metrics? metrics) "expected a valid instance for metrics"))
(defmethod ig/init-key ::routes
[_ {:keys [::metrics]}]
["/metrics" {:handler (get-handler metrics)
:allowed-methods #{:get}}])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Implementation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def default-empty-labels (into-array String []))
(def default-quantiles
[[0.5 0.01]
[0.90 0.01]
[0.99 0.001]])
(def default-histogram-buckets
[1 5 10 25 50 75 100 250 500 750 1000 2500 5000 7500])
(defmulti run-collector! (fn [mdef _] (::mdef/type mdef)))
(defmulti create-collector ::mdef/type)
(defn run!
[instance & {:keys [id] :as params}]
(assert (metrics? instance) "expected valid metrics instance")
(when-let [mobj (get-collector instance id)]
(run-collector! mobj params)
true))
(defn- create-registry
[]
(let [registry (CollectorRegistry.)]
(DefaultExports/register registry)
registry))
(defn- is-array?
[o]
(let [oc (class o)]
(and (.isArray ^Class oc)
(= (.getComponentType oc) String))))
(defmethod run-collector! :counter
[{:keys [::mdef/instance]} {:keys [inc labels] :or {inc 1 labels default-empty-labels}}]
(let [instance (.labels ^Counter instance (if (is-array? labels) labels (into-array String labels)))]
(.inc ^Counter$Child instance (double inc))))
(defmethod run-collector! :gauge
[{:keys [::mdef/instance]} {:keys [inc dec labels val] :or {labels default-empty-labels}}]
(let [instance (.labels ^Gauge instance (if (is-array? labels) labels (into-array String labels)))]
(cond (number? inc) (.inc ^Gauge$Child instance (double inc))
(number? dec) (.dec ^Gauge$Child instance (double dec))
(number? val) (.set ^Gauge$Child instance (double val)))))
(defmethod run-collector! :summary
[{:keys [::mdef/instance]} {:keys [val labels] :or {labels default-empty-labels}}]
(let [instance (.labels ^Summary instance (if (is-array? labels) labels (into-array String labels)))]
(.observe ^Summary$Child instance val)))
(defmethod run-collector! :histogram
[{:keys [::mdef/instance]} {:keys [val labels] :or {labels default-empty-labels}}]
(let [instance (.labels ^Histogram instance (if (is-array? labels) labels (into-array String labels)))]
(.observe ^Histogram$Child instance val)))
(defmethod create-collector :counter
[{::mdef/keys [name help reg labels]
::keys [registry]
:as props}]
(let [registry (or registry reg)
instance (.. (Counter/build)
(name name)
(help help))]
(when (seq labels)
(.labelNames instance (into-array String labels)))
(assoc props ::mdef/instance (.register instance registry))))
(defmethod create-collector :gauge
[{::mdef/keys [name help reg labels]
::keys [registry]
:as props}]
(let [registry (or registry reg)
instance (.. (Gauge/build)
(name name)
(help help))]
(when (seq labels)
(.labelNames instance (into-array String labels)))
(assoc props ::mdef/instance (.register instance registry))))
(defmethod create-collector :summary
[{::mdef/keys [name help reg labels max-age quantiles buckets]
::keys [registry]
:or {max-age 3600 buckets 12 quantiles default-quantiles}
:as props}]
(let [registry (or registry reg)
builder (doto (Summary/build)
(.name name)
(.help help))]
(when (seq quantiles)
(.maxAgeSeconds ^Summary$Builder builder ^long max-age)
(.ageBuckets ^Summary$Builder builder buckets))
(doseq [[q e] quantiles]
(.quantile ^Summary$Builder builder q e))
(when (seq labels)
(.labelNames ^Summary$Builder builder (into-array String labels)))
(assoc props ::mdef/instance (.register ^Summary$Builder builder registry))))
(defmethod create-collector :histogram
[{::mdef/keys [name help reg labels buckets]
::keys [registry]
:or {buckets default-histogram-buckets}
:as props}]
(let [registry (or registry reg)
instance (doto (Histogram/build)
(.name name)
(.help help)
(.buckets (into-array Double/TYPE buckets)))]
(when (seq labels)
(.labelNames instance (into-array String labels)))
(assoc props ::mdef/instance (.register instance registry))))