mirror of
https://github.com/penpot/penpot.git
synced 2026-08-06 21:08:34 +00:00
🔧 Add exporter metrics
This commit is contained in:
parent
432de87a7e
commit
c43969535b
@ -50,7 +50,11 @@
|
||||
[:wasm-dir {:optional true} :string]
|
||||
;; Byte budget (in MB) for the WASM image cache; least-recently-used
|
||||
;; images are evicted between requests once the store exceeds it.
|
||||
[:wasm-image-cache-mb {:optional true} ::sm/int]])
|
||||
[:wasm-image-cache-mb {:optional true} ::sm/int]
|
||||
;; Expose unauthenticated /metrics (Prometheus text format) with
|
||||
;; per-render instrumentation. Development/measurement aid; off by
|
||||
;; default so production deployments are unaffected.
|
||||
[:metrics {:optional true} :boolean]])
|
||||
|
||||
(def ^:private decode-config
|
||||
(sm/decoder schema:config sm/string-transformer))
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
[app.common.transit :as t]
|
||||
[app.config :as cf]
|
||||
[app.handlers :as handlers]
|
||||
[app.metrics :as mtx]
|
||||
[cuerdas.core :as str]
|
||||
[lambdaisland.uri :as u]
|
||||
[promesa.core :as p]))
|
||||
@ -135,6 +136,21 @@
|
||||
:response/body "OK")
|
||||
(handler exchange))))
|
||||
|
||||
(defn- wrap-metrics
|
||||
"Serve in-process render metrics on /metrics (Prometheus text format).
|
||||
Unauthenticated, so it is opt-in via the `:metrics` config (PENPOT_METRICS
|
||||
env var); when disabled the handler chain is left untouched."
|
||||
[handler]
|
||||
(if-not (cf/get :metrics)
|
||||
handler
|
||||
(fn [{:keys [:request/path] :as exchange}]
|
||||
(if (= path "/metrics")
|
||||
(assoc exchange
|
||||
:response/status 200
|
||||
:response/headers {"content-type" "text/plain; version=0.0.4"}
|
||||
:response/body (mtx/export-text))
|
||||
(handler exchange)))))
|
||||
|
||||
(defn- create-adapter
|
||||
[handler]
|
||||
(fn [req res]
|
||||
@ -161,6 +177,7 @@
|
||||
[]
|
||||
(let [handler (-> handlers/handler
|
||||
(wrap-health)
|
||||
(wrap-metrics)
|
||||
(wrap-auth "auth-token")
|
||||
(wrap-response-format)
|
||||
(wrap-params)
|
||||
|
||||
127
exporter/src/app/metrics.cljs
Normal file
127
exporter/src/app/metrics.cljs
Normal file
@ -0,0 +1,127 @@
|
||||
;; 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 Sucursal en España SL
|
||||
|
||||
(ns app.metrics
|
||||
"Minimal in-process Prometheus metrics (text exposition format, no
|
||||
external deps). Records per-render counters and duration/CPU histograms
|
||||
labeled by render backend (`wasm` vs `browser`) and export type, so the
|
||||
headless and browser export paths can be compared in Grafana."
|
||||
(:require
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private bucket-bounds
|
||||
[0.5 1 2.5 5 10 30 60 120])
|
||||
|
||||
;; {[backend type] {:count n :errors n :sum secs :cpu secs :buckets [n...]}}
|
||||
(defonce ^:private state (atom {}))
|
||||
|
||||
(defn- empty-entry
|
||||
[]
|
||||
{:count 0
|
||||
:errors 0
|
||||
:sum 0
|
||||
:cpu 0
|
||||
:buckets (vec (repeat (count bucket-bounds) 0))})
|
||||
|
||||
(defn- update-entry
|
||||
[entry duration cpu error?]
|
||||
(let [entry (or entry (empty-entry))]
|
||||
(-> entry
|
||||
(update :count inc)
|
||||
(cond-> error? (update :errors inc))
|
||||
(update :sum + duration)
|
||||
(update :cpu + cpu)
|
||||
;; increment every bucket whose upper bound covers this duration;
|
||||
;; that keeps the stored counts cumulative, as prometheus expects.
|
||||
(update :buckets
|
||||
(fn [buckets]
|
||||
(vec (map-indexed
|
||||
(fn [i count]
|
||||
(if (<= duration (nth bucket-bounds i))
|
||||
(inc count)
|
||||
count))
|
||||
buckets)))))))
|
||||
|
||||
(defn observe-render!
|
||||
[backend type duration-s cpu-s error?]
|
||||
(swap! state update [backend (name type)] update-entry duration-s cpu-s error?))
|
||||
|
||||
(defn- elapsed
|
||||
"Seconds elapsed and process CPU seconds consumed since `t0`/`c0`."
|
||||
[t0 c0]
|
||||
(let [duration (/ (js/Number (- (js/process.hrtime.bigint) t0)) 1e9)
|
||||
cusage (js/process.cpuUsage c0)
|
||||
cpu (/ (+ (unchecked-get cusage "user")
|
||||
(unchecked-get cusage "system"))
|
||||
1e6)]
|
||||
[duration cpu]))
|
||||
|
||||
(defn with-render-metrics
|
||||
"Runs `thunk` (returning a promise) and records its wall-clock duration
|
||||
and the process CPU delta under the given backend/type labels. CPU is a
|
||||
whole-process delta, so concurrent renders bleed into each other's
|
||||
numbers; fine for the low-concurrency comparison use case."
|
||||
[backend type thunk]
|
||||
(let [t0 (js/process.hrtime.bigint)
|
||||
c0 (js/process.cpuUsage)]
|
||||
(-> (p/do (thunk))
|
||||
(p/then (fn [result]
|
||||
(let [[duration cpu] (elapsed t0 c0)]
|
||||
(observe-render! backend type duration cpu false)
|
||||
result)))
|
||||
(p/catch (fn [cause]
|
||||
(let [[duration cpu] (elapsed t0 c0)]
|
||||
(observe-render! backend type duration cpu true)
|
||||
(p/rejected cause)))))))
|
||||
|
||||
(defn- labels-str
|
||||
[backend type & extra]
|
||||
(str "{backend=\"" backend "\",type=\"" type "\""
|
||||
(str/join "" extra)
|
||||
"}"))
|
||||
|
||||
(defn export-text
|
||||
"Renders all recorded metrics in the Prometheus text exposition format."
|
||||
[]
|
||||
(let [entries @state
|
||||
lines
|
||||
(concat
|
||||
["# HELP penpot_exporter_render_total Total renders processed."
|
||||
"# TYPE penpot_exporter_render_total counter"]
|
||||
(for [[[backend type] entry] entries]
|
||||
(str "penpot_exporter_render_total" (labels-str backend type) " " (:count entry)))
|
||||
|
||||
["# HELP penpot_exporter_render_errors_total Renders that failed."
|
||||
"# TYPE penpot_exporter_render_errors_total counter"]
|
||||
(for [[[backend type] entry] entries]
|
||||
(str "penpot_exporter_render_errors_total" (labels-str backend type) " " (:errors entry)))
|
||||
|
||||
["# HELP penpot_exporter_render_cpu_seconds_total Process CPU consumed during renders."
|
||||
"# TYPE penpot_exporter_render_cpu_seconds_total counter"]
|
||||
(for [[[backend type] entry] entries]
|
||||
(str "penpot_exporter_render_cpu_seconds_total" (labels-str backend type) " " (:cpu entry)))
|
||||
|
||||
["# HELP penpot_exporter_render_duration_seconds Render wall-clock duration."
|
||||
"# TYPE penpot_exporter_render_duration_seconds histogram"]
|
||||
(mapcat
|
||||
(fn [[[backend type] entry]]
|
||||
(concat
|
||||
(map-indexed
|
||||
(fn [i bound]
|
||||
(str "penpot_exporter_render_duration_seconds_bucket"
|
||||
(labels-str backend type ",le=\"" bound "\"")
|
||||
" " (nth (:buckets entry) i)))
|
||||
bucket-bounds)
|
||||
[(str "penpot_exporter_render_duration_seconds_bucket"
|
||||
(labels-str backend type ",le=\"+Inf\"")
|
||||
" " (:count entry))
|
||||
(str "penpot_exporter_render_duration_seconds_sum"
|
||||
(labels-str backend type) " " (:sum entry))
|
||||
(str "penpot_exporter_render_duration_seconds_count"
|
||||
(labels-str backend type) " " (:count entry))]))
|
||||
entries))]
|
||||
(str (str/join "\n" lines) "\n")))
|
||||
@ -10,6 +10,7 @@
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.metrics :as mtx]
|
||||
[app.renderer.bitmap :as rb]
|
||||
[app.renderer.pdf :as rp]
|
||||
[app.renderer.svg :as rs]
|
||||
@ -45,18 +46,21 @@
|
||||
;; Opt-in headless path: when an export is flagged `:is-wasm` AND the
|
||||
;; `:wasm-headless` config is enabled, render with the in-process Skia/WASM
|
||||
;; pipeline (no browser). Off by default, so existing behavior is unchanged.
|
||||
(let [headless? (and is-wasm (cf/get :wasm-headless))]
|
||||
(let [headless? (and is-wasm (cf/get :wasm-headless))
|
||||
backend (if headless? "wasm" "browser")]
|
||||
(l/info :hint "render"
|
||||
:type type
|
||||
:is-wasm (boolean is-wasm)
|
||||
:wasm-headless (boolean (cf/get :wasm-headless))
|
||||
:backend (if headless? "wasm" "browser"))
|
||||
(if headless?
|
||||
(rw/render params on-object)
|
||||
(case type
|
||||
:png (rb/render params on-object)
|
||||
:jpeg (rb/render params on-object)
|
||||
:webp (rb/render params on-object)
|
||||
:pdf (rp/render params on-object)
|
||||
:svg (rs/render params on-object)))))
|
||||
:backend backend)
|
||||
(mtx/with-render-metrics backend type
|
||||
(fn []
|
||||
(if headless?
|
||||
(rw/render params on-object)
|
||||
(case type
|
||||
:png (rb/render params on-object)
|
||||
:jpeg (rb/render params on-object)
|
||||
:webp (rb/render params on-object)
|
||||
:pdf (rp/render params on-object)
|
||||
:svg (rs/render params on-object)))))))
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user