From 1aef0288e75db6795004ff113e99d6c2bd5d2233 Mon Sep 17 00:00:00 2001 From: miyuko Date: Mon, 22 Sep 2025 14:33:25 +0100 Subject: [PATCH] Add page operation metrics and expose them in Prometheus text format. --- conf/config.example.toml | 1 + go.mod | 8 ++++++++ go.sum | 16 ++++++++++++++++ src/config.go | 7 ++++--- src/main.go | 12 ++++++------ src/observe.go | 5 +++++ src/pages.go | 39 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 79 insertions(+), 9 deletions(-) diff --git a/conf/config.example.toml b/conf/config.example.toml index e51c0be..5ce5604 100644 --- a/conf/config.example.toml +++ b/conf/config.example.toml @@ -8,6 +8,7 @@ log-format = "datetime+message" pages = "tcp/:3000" caddy = "tcp/:3001" health = "tcp/:3002" +metrics = "tcp/:3003" [[wildcard]] # non-default section domain = "codeberg.page" diff --git a/go.mod b/go.mod index f710a5c..ad09124 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,8 @@ require ( dario.cat/mergo v1.0.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.6.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -36,11 +38,16 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/minio/crc64nvme v1.0.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/rs/xid v1.6.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect @@ -48,6 +55,7 @@ require ( github.com/tj/assert v0.0.3 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/crypto v0.41.0 // indirect golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect golang.org/x/net v0.43.0 // indirect diff --git a/go.sum b/go.sum index 1b7b410..0f0f1b8 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,12 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 h1:6lhrsTEnloDPXyeZBvSYvQf8u86jbKehZPVDDlkgDl4= github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= @@ -70,6 +74,8 @@ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU= github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= @@ -84,6 +90,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= @@ -113,6 +127,8 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA= diff --git a/src/config.go b/src/config.go index daf9ed9..10a50a7 100644 --- a/src/config.go +++ b/src/config.go @@ -45,9 +45,10 @@ type Config struct { } type ServerConfig struct { - Pages string `toml:"pages" default:"tcp/:3000"` - Caddy string `toml:"caddy" default:"tcp/:3001"` - Health string `toml:"health" default:"tcp/:3002"` + Pages string `toml:"pages" default:"tcp/:3000"` + Caddy string `toml:"caddy" default:"tcp/:3001"` + Health string `toml:"health" default:"tcp/:3002"` + Metrics string `toml:"metrics" default:"tcp/:3003"` } type WildcardConfig struct { diff --git a/src/main.go b/src/main.go index 93e499b..1168238 100644 --- a/src/main.go +++ b/src/main.go @@ -50,10 +50,8 @@ func panicHandler(handler http.Handler) http.Handler { }) } -func serve(listener net.Listener, serve func(http.ResponseWriter, *http.Request)) { +func serve(listener net.Listener, handler http.Handler) { if listener != nil { - var handler http.Handler - handler = http.HandlerFunc(serve) handler = ObserveHTTPHandler(handler) handler = panicHandler(handler) @@ -148,6 +146,7 @@ func main() { pagesListener := listen("pages", config.Server.Pages) caddyListener := listen("caddy", config.Server.Caddy) healthListener := listen("health", config.Server.Health) + metricsListener := listen("metrics", config.Server.Metrics) if err := ConfigureBackend(&config.Storage); err != nil { log.Fatalln(err) @@ -157,9 +156,10 @@ func main() { log.Fatalln(err) } - go serve(pagesListener, ServePages) - go serve(caddyListener, ServeCaddy) - go serve(healthListener, ServeHealth) + go serve(pagesListener, http.HandlerFunc(ServePages)) + go serve(caddyListener, http.HandlerFunc(ServeCaddy)) + go serve(healthListener, http.HandlerFunc(ServeHealth)) + go serve(metricsListener, NewMetricsHTTPHandler()) if config.Insecure { log.Println("serve: ready (INSECURE)") diff --git a/src/observe.go b/src/observe.go index c8167cb..64a45c4 100644 --- a/src/observe.go +++ b/src/observe.go @@ -6,6 +6,7 @@ import ( "runtime/debug" "github.com/honeybadger-io/honeybadger-go" + "github.com/prometheus/client_golang/prometheus/promhttp" ) func hasHoneybadger() bool { @@ -25,3 +26,7 @@ func ObserveHTTPHandler(handler http.Handler) http.Handler { } return handler } + +func NewMetricsHTTPHandler() http.Handler { + return promhttp.Handler() +} diff --git a/src/pages.go b/src/pages.go index a7d3d8d..5610094 100644 --- a/src/pages.go +++ b/src/pages.go @@ -14,14 +14,51 @@ import ( "path" "strings" "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" ) const notFoundPage = "404.html" +var ( + siteUpdatesCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "git_pages_site_updates", + Help: "Count of site updates in total", + }, []string{"via"}) + siteUpdateOkCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "git_pages_site_update_ok", + Help: "Count of successful site updates", + }, []string{"outcome"}) + siteUpdateErrorCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "git_pages_site_update_error", + Help: "Count of failed site updates", + }, []string{"cause"}) +) + func makeWebRoot(host string, projectName string) string { return fmt.Sprintf("%s/%s", strings.ToLower(host), projectName) } +func reportSiteUpdate(via string, result *UpdateResult) { + siteUpdatesCount.With(prometheus.Labels{"via": via}).Inc() + + switch result.outcome { + case UpdateError: + siteUpdateErrorCount.With(prometheus.Labels{"cause": "other"}).Inc() + case UpdateTimeout: + siteUpdateErrorCount.With(prometheus.Labels{"cause": "timeout"}).Inc() + case UpdateNoChange: + // nothing to report + case UpdateCreated: + siteUpdateOkCount.With(prometheus.Labels{"outcome": "created"}).Inc() + case UpdateReplaced: + siteUpdateOkCount.With(prometheus.Labels{"outcome": "replaced"}).Inc() + case UpdateDeleted: + siteUpdateOkCount.With(prometheus.Labels{"outcome": "deleted"}).Inc() + } +} + func getPage(w http.ResponseWriter, r *http.Request) error { var err error var sitePath string @@ -266,6 +303,7 @@ func putPage(w http.ResponseWriter, r *http.Request) error { } else { fmt.Fprintln(w, "internal error") } + reportSiteUpdate("rest", &result) return nil } @@ -401,6 +439,7 @@ func postPage(w http.ResponseWriter, r *http.Request) error { fmt.Fprintf(w, "- %s\n", problem) } } + reportSiteUpdate("webhook", &result) return nil }