diff --git a/.goreleaser.yml b/.goreleaser.yml index 814de3a8..8cc69bea 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -14,10 +14,12 @@ builds: - arm goos: - linux - # - windows - # - freebsd - # - darwin - #ignore: + - windows + - freebsd + - darwin + ignore: + - goarch: '386' + goos: windows # - goarch: arm # goos: windows # - goarch: arm64 diff --git a/Makefile b/Makefile index d68edce4..6b140de3 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,14 @@ DOCKER_ACCOUNT = cherts APPNAME = pgscv -APPOS = linux -#APPOS = ${GOOS} +APPOS=${GOOS} + +OS_NAME := $(shell uname -s | tr A-Z a-z) + +ifneq (,$(findstring msys,$(OS_NAME))) +APPEXT=.exe +else +APPEXT= +endif TAG_COMMIT := $(shell git rev-list --abbrev-commit --tags --max-count=1) TAG := $(shell git describe --abbrev=0 --tags ${TAG_COMMIT} 2>/dev/null || true) @@ -43,7 +50,7 @@ help: ## Display this help screen @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " * \033[36m%-25s\033[0m %s\n", $$1, $$2}' clean: ## Clean - rm -f ./bin/${APPNAME} ./bin/${APPNAME}.tar.gz ./bin/${APPNAME}.version ./bin/${APPNAME}.sha256 + rm -f ./bin/${APPNAME}${APPEXT} ./bin/${APPNAME}.tar.gz ./bin/${APPNAME}.version ./bin/${APPNAME}.sha256 rm -rf ./bin go-update: ## Update go mod @@ -71,7 +78,7 @@ race: dep ## Run data race detector build: dep ## Build mkdir -p ./bin - CGO_ENABLED=0 GOOS=${APPOS} GOARCH=${GOARCH} go build ${LDFLAGS} -o bin/${APPNAME} ./cmd + CGO_ENABLED=0 GOOS=${APPOS} GOARCH=${GOARCH} go build ${LDFLAGS} -o bin/${APPNAME}${APPEXT} ./cmd build-beta: dep ## Build beta mkdir -p ./bin diff --git a/README.md b/README.md index 6b4edba6..cd7d03ac 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ This project is a continuation of the development of the original pgSCV by [Alex ### Features - **Supported services:** support collecting metrics of PostgreSQL, Pgbouncer and Patroni. -- **OS metrics:** support collecting metrics of operating system (only Linux). +- **OS metrics:** support collecting metrics of operating system (Linux, Windows, MacOS, FreeBSD). - **Discovery and monitoring Cloud Managed Databases:** Yandex Managed Service for PostgreSQL ([see documentation](https://github.com/cherts/pgscv/wiki/Monitoring-Cloud-Managed-Databases)). - **Support Prometheus service discovery.** `/targets` endpoint is used to discover all monitoring services ([see documentation](https://github.com/cherts/pgscv/wiki/Service-discovery)) - **Concurrency limitting support** It is possible to limit the parallel collection of monitoring data from the database to control the load created by the exporter. ([see documentatio](https://github.com/cherts/pgscv/wiki/Concurrency)). @@ -30,7 +30,8 @@ This project is a continuation of the development of the original pgSCV by [Alex block devices, network interfaces, filesystems, users, databases, etc. ### Requirements -- Can run on Linux only; can connect to remote services running on other OS/PaaS. +- Can run on Linux, Windows, MacOS, FreeBSD only. +- Can connect to remote services running on other OS/PaaS. - Requisites for connecting to the services, such as login and password. - Database user should have privileges for executing stats functions and reading views. For more details see [security considerations](https://github.com/cherts/pgscv/wiki/Security-considerations). @@ -39,7 +40,7 @@ This project is a continuation of the development of the original pgSCV by [Alex Download the archive from [releases](https://github.com/cherts/pgscv/releases). Unpack the archive. Create minimum config file. Start pgSCV systemd service under `postgres` user. ```bash -curl -s -L https://github.com/cherts/pgscv/releases/download/v1.0.0/pgscv_1.0.0_linux_$(uname -m).tar.gz -o - | tar xzf - -C /tmp && \ +curl -s -L https://github.com/cherts/pgscv/releases/download/v1.0.0/pgscv_1.0.0_$(uname -s | tr A-Z a-z)_$(uname -m).tar.gz -o - | tar xzf - -C /tmp && \ mv /tmp/pgscv.yaml /etc && \ mv /tmp/pgscv.service /etc/systemd/system && \ mv /tmp/pgscv.default /etc/default/pgscv && \ diff --git a/README.ru.md b/README.ru.md index cd3107ca..a900abe0 100644 --- a/README.ru.md +++ b/README.ru.md @@ -11,7 +11,7 @@ ### Основные возможности - **Поддерживаемые сервисы**: поддержка сбора показателей работы PostgreSQL, Pgbouncer и Patroni; -- **Метрики ОС:** поддержка сбора показателей работы операционной системы (только Linux); +- **Метрики ОС:** поддержка сбора показателей работы операционной системы (Linux, Windows, MacOS, FreeBSD); - **Обнаружение и мониторинг Облачных управляемых баз данных** Yandex Managed Service for PostgreSQL ([смотри документацию](https://github.com/cherts/pgscv/wiki/Monitoring-Cloud-Managed-Databases)); - **Поддержка обнаружения сервисов мониторинга** Через специальный эндпойнт `/targets` можно производить обнаружение всех сервисов мониторинга ([смотри документацию](https://github.com/cherts/pgscv/wiki/Service-discovery)) - **Поддержка контроля параллелизма** Можно ограничить возможности параллельного сбора данных мониторинга из базы данных для контроля нагрузки создаваемой экспортером ([смотри документацию](https://github.com/cherts/pgscv/wiki/Concurrency)). @@ -22,7 +22,7 @@ - **Коллекторные фильтры**: Коллекторы можно настроить так, чтобы они пропускали сбор метрик на основе значений меток, например блочные устройства, сетевые интерфейсы, файловые системы, пользователи, базы данных и т.д.; ### Системные требования: -- Может работать только в ОС Linux; +- Может работать только в ОС Linux, Windows, MacOS, FreeBSD; - Может подключаться к удаленным сервисам, работающим на другой ОС/PaaS; - Необходимы данные для подключения к сервисам/базам, такие как адрес, логин и пароль; - Пользователь базы данных должен иметь права на выполнение статистических функций и чтение представлений. @@ -32,7 +32,7 @@ Загрузите архив со страницы [releases](https://github.com/cherts/pgscv/releases). Распакуйте архив. Создайте минимальный файл конфигураации. Запустите pgSCV под пользователем postgres. ```bash -curl -s -L https://github.com/cherts/pgscv/releases/download/v1.0.0/pgscv_1.0.0_linux_$(uname -m).tar.gz -o - | tar xzf - -C /tmp && \ +curl -s -L https://github.com/cherts/pgscv/releases/download/v1.0.0/pgscv_1.0.0_$(uname -s | tr A-Z a-z)_$(uname -m).tar.gz -o - | tar xzf - -C /tmp && \ mv /tmp/pgscv.yaml /etc && \ mv /tmp/pgscv.service /etc/systemd/system && \ mv /tmp/pgscv.default /etc/default/pgscv && \ diff --git a/deploy/demo-lab/grafana/dashboards/pgSCV_System.json b/deploy/demo-lab/grafana/dashboards/pgSCV_System.json index 6d30f876..45ae531c 100644 --- a/deploy/demo-lab/grafana/dashboards/pgSCV_System.json +++ b/deploy/demo-lab/grafana/dashboards/pgSCV_System.json @@ -368,7 +368,7 @@ "uid": "DS_VM_01" }, "exemplar": false, - "expr": "node_memory_MemFree{instance=\"$instance\"} + node_memory_Cached{instance=\"$instance\"} + node_memory_Buffers{instance=\"$instance\"}", + "expr": "node_memory_available{instance=\"$instance\"}", "instant": false, "interval": "", "legendFormat": "", @@ -707,7 +707,7 @@ "uid": "DS_VM_01" }, "editorMode": "code", - "expr": "sum by (device) (rate(node_disk_io_time_seconds_total{instance=\"$instance\"}[1m])) / on() group_left() rate(node_uptime_up_seconds_total{instance=\"$instance\"}[1m]) * 100", + "expr": "sum by (device) (rate(node_disk_io_time_seconds_total{instance=\"$instance\"}[1m])) / on() group_left() rate(node_uptime_seconds{instance=\"$instance\"}[1m]) * 100", "instant": false, "legendFormat": "__auto", "range": true, @@ -874,7 +874,7 @@ "uid": "DS_VM_01" }, "exemplar": true, - "expr": "avg_over_time(node_memory_MemFree{instance=\"$instance\"}[1m])", + "expr": "avg_over_time(node_memory_free{instance=\"$instance\"}[1m])", "interval": "", "legendFormat": "free", "refId": "A" @@ -885,7 +885,7 @@ "uid": "${ds}" }, "exemplar": true, - "expr": "avg_over_time(node_memory_Buffers{instance=\"$instance\"}[1m])", + "expr": "avg_over_time(node_memory_buffers{instance=\"$instance\"}[1m])", "hide": false, "interval": "", "legendFormat": "buffers", @@ -897,7 +897,7 @@ "uid": "DS_VM_01" }, "exemplar": true, - "expr": "avg_over_time(node_memory_Cached{instance=\"$instance\"}[1m])", + "expr": "avg_over_time(node_memory_cached{instance=\"$instance\"}[1m])", "hide": false, "interval": "", "legendFormat": "cached", @@ -910,7 +910,7 @@ }, "editorMode": "code", "exemplar": true, - "expr": "avg_over_time(node_memory_MemUsed{instance=\"$instance\"}[1m])", + "expr": "avg_over_time(node_memory_used{instance=\"$instance\"}[1m])", "hide": false, "interval": "", "legendFormat": "used", diff --git a/deploy/grafana/pgSCV_System.json b/deploy/grafana/pgSCV_System.json index 148612a7..81afb8c9 100644 --- a/deploy/grafana/pgSCV_System.json +++ b/deploy/grafana/pgSCV_System.json @@ -411,7 +411,7 @@ "uid": "${DS_VICTORIAMETRICS-PGSCV}" }, "exemplar": false, - "expr": "node_memory_MemFree{instance=\"$instance\"} + node_memory_Cached{instance=\"$instance\"} + node_memory_Buffers{instance=\"$instance\"}", + "expr": "node_memory_available{instance=\"$instance\"}", "instant": false, "interval": "", "legendFormat": "", @@ -750,7 +750,7 @@ "uid": "${DS_VICTORIAMETRICS-PGSCV}" }, "editorMode": "code", - "expr": "sum by (device) (rate(node_disk_io_time_seconds_total{instance=\"$instance\"}[1m])) / on() group_left() rate(node_uptime_up_seconds_total{instance=\"$instance\"}[1m]) * 100", + "expr": "sum by (device) (rate(node_disk_io_time_seconds_total{instance=\"$instance\"}[1m])) / on() group_left() rate(node_uptime_seconds{instance=\"$instance\"}[1m]) * 100", "instant": false, "legendFormat": "__auto", "range": true, @@ -917,7 +917,7 @@ "uid": "${DS_VICTORIAMETRICS-PGSCV}" }, "exemplar": true, - "expr": "avg_over_time(node_memory_MemFree{instance=\"$instance\"}[1m])", + "expr": "avg_over_time(node_memory_free{instance=\"$instance\"}[1m])", "interval": "", "legendFormat": "free", "refId": "A" @@ -928,7 +928,7 @@ "uid": "${ds}" }, "exemplar": true, - "expr": "avg_over_time(node_memory_Buffers{instance=\"$instance\"}[1m])", + "expr": "avg_over_time(node_memory_buffers{instance=\"$instance\"}[1m])", "hide": false, "interval": "", "legendFormat": "buffers", @@ -940,7 +940,7 @@ "uid": "${DS_VICTORIAMETRICS-PGSCV}" }, "exemplar": true, - "expr": "avg_over_time(node_memory_Cached{instance=\"$instance\"}[1m])", + "expr": "avg_over_time(node_memory_cached{instance=\"$instance\"}[1m])", "hide": false, "interval": "", "legendFormat": "cached", @@ -953,7 +953,7 @@ }, "editorMode": "code", "exemplar": true, - "expr": "avg_over_time(node_memory_MemUsed{instance=\"$instance\"}[1m])", + "expr": "avg_over_time(node_memory_used{instance=\"$instance\"}[1m])", "hide": false, "interval": "", "legendFormat": "used", diff --git a/go.mod b/go.mod index 643bbf6a..282142fc 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,8 @@ require ( gopkg.in/yaml.v2 v2.4.0 ) +require github.com/shirou/gopsutil/v4 v4.25.11 + require ( github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf github.com/go-playground/validator/v10 v10.30.1 @@ -28,9 +30,11 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/ebitengine/purego v0.9.1 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/ghodss/yaml v1.0.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect @@ -41,14 +45,19 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/kr/text v0.1.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.19.2 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/text v0.33.0 // indirect diff --git a/go.sum b/go.sum index e7b6d77a..5a967ed1 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= @@ -23,6 +25,9 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -64,6 +69,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -80,6 +87,8 @@ github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JX 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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 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= @@ -95,6 +104,8 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAXZILTY= +github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -108,12 +119,18 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yandex-cloud/go-genproto v0.43.0 h1:HjBesEmCN8ZOhjjh8gs605vvi9/MBJAW3P20OJ4iQnw= github.com/yandex-cloud/go-genproto v0.43.0/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo= github.com/yandex-cloud/go-sdk v0.30.0 h1:bHhUlkfaLbcNQvdfxMpRnft+tbCFtLRUFrZ3rC1hqgM= github.com/yandex-cloud/go-sdk v0.30.0/go.mod h1:em/KXj64MBSNoe9qQs/X0Ua9Wp460t9qvIIim+tT7OE= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= @@ -140,8 +157,11 @@ golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= diff --git a/internal/collector/any_cpu.go b/internal/collector/any_cpu.go new file mode 100644 index 00000000..3e4012c9 --- /dev/null +++ b/internal/collector/any_cpu.go @@ -0,0 +1,116 @@ +package collector + +import ( + "context" + "fmt" + "os/exec" + "strconv" + "strings" + + "github.com/cherts/pgscv/internal/log" + "github.com/cherts/pgscv/internal/model" + "github.com/prometheus/client_golang/prometheus" + "github.com/shirou/gopsutil/v4/cpu" +) + +type cpuCollector struct { + systicks float64 + cpu typedDesc + cpuAll typedDesc + cpuGuest typedDesc + physicalCnt typedDesc + logicalCnt typedDesc +} + +// NewCPUCollector returns a new Collector exposing kernel/system statistics. +func NewCPUCollector(constLabels labels, settings model.CollectorSettings) (Collector, error) { + cmdOutput, err := exec.Command("getconf", "CLK_TCK").Output() + if err != nil { + return nil, fmt.Errorf("determine clock frequency failed: %s", err) + } + + value := strings.TrimSpace(string(cmdOutput)) + systicks, err := strconv.ParseFloat(value, 64) + if err != nil { + return nil, fmt.Errorf("invalid input: parse '%s' failed: %w", value, err) + } + + c := &cpuCollector{ + systicks: systicks, + cpu: newBuiltinTypedDesc( + descOpts{"node", "cpu", "seconds_total", "Seconds the CPUs spent in each mode.", 0}, + prometheus.CounterValue, + []string{"mode"}, constLabels, + settings.Filters, + ), + cpuAll: newBuiltinTypedDesc( + descOpts{"node", "cpu", "seconds_all_total", "Seconds the CPUs spent in all modes.", 0}, + prometheus.CounterValue, + nil, constLabels, + settings.Filters, + ), + cpuGuest: newBuiltinTypedDesc( + descOpts{"node", "cpu", "guest_seconds_total", "Seconds the CPUs spent in guests (VMs) for each mode.", 0}, + prometheus.CounterValue, + []string{"mode"}, constLabels, + settings.Filters, + ), + physicalCnt: newBuiltinTypedDesc( + descOpts{"node", "cpu", "physical_core", "Total physical number of core.", 0}, + prometheus.CounterValue, + nil, constLabels, + settings.Filters, + ), + logicalCnt: newBuiltinTypedDesc( + descOpts{"node", "cpu", "logical_core", "Total logical number of core.", 0}, + prometheus.CounterValue, + nil, constLabels, + settings.Filters, + ), + } + return c, nil +} + +// Update implements Collector and exposes cpu related metrics from /proc/stat and /sys/.../cpu/. +func (c *cpuCollector) Update(_ context.Context, _ Config, ch chan<- prometheus.Metric) error { + cpuStat, err := cpu.Times(false) + if err != nil { + return fmt.Errorf("collect cpu usage stats failed: %s; skip", err) + } + + // Collected time represents summary time spent by ALL cpu cores. + for _, cpuData := range cpuStat { + ch <- c.cpu.newConstMetric(cpuData.User, "user") + ch <- c.cpu.newConstMetric(cpuData.Nice, "nice") + ch <- c.cpu.newConstMetric(cpuData.System, "system") + ch <- c.cpu.newConstMetric(cpuData.Idle, "idle") + ch <- c.cpu.newConstMetric(cpuData.Iowait, "iowait") + ch <- c.cpu.newConstMetric(cpuData.Irq, "irq") + ch <- c.cpu.newConstMetric(cpuData.Softirq, "softirq") + ch <- c.cpu.newConstMetric(cpuData.Steal, "steal") + ch <- c.cpuAll.newConstMetric(cpuData.User + cpuData.Nice + cpuData.System + cpuData.Idle + cpuData.Iowait + cpuData.Irq + cpuData.Softirq + cpuData.Steal) + // Guest CPU is also accounted for in stat.user and stat.nice, expose these as separate metrics. + ch <- c.cpuGuest.newConstMetric(cpuData.Guest, "user") + ch <- c.cpuGuest.newConstMetric(cpuData.GuestNice, "nice") + } + + // Get physical and logical CPU core + physicalCnt, err := cpu.Counts(false) + if err != nil { + return err + } + if physicalCnt == 0 { + log.Warnf("could not get physical CPU counts: %v", physicalCnt) + } + logicalCnt, err := cpu.Counts(true) + if err != nil { + return err + } + if logicalCnt == 0 { + log.Warnf("could not get physical CPU counts: %v", logicalCnt) + } + ch <- c.physicalCnt.newConstMetric(float64(physicalCnt)) + ch <- c.logicalCnt.newConstMetric(float64(logicalCnt)) + + return nil +} diff --git a/internal/collector/any_cpu_test.go b/internal/collector/any_cpu_test.go new file mode 100644 index 00000000..72f0bfbe --- /dev/null +++ b/internal/collector/any_cpu_test.go @@ -0,0 +1,29 @@ +package collector + +import ( + "testing" + + "github.com/shirou/gopsutil/v4/cpu" + "github.com/stretchr/testify/assert" +) + +func TestCPUCollector_Update(t *testing.T) { + var input = pipelineInput{ + required: []string{ + "node_cpu_seconds_total", + "node_cpu_seconds_all_total", + "node_cpu_guest_seconds_total", + "node_cpu_physical_core", + "node_cpu_logical_core", + }, + collector: NewCPUCollector, + } + + pipeline(t, input) +} + +func Test_CpuInfo(t *testing.T) { + info, err := cpu.Times(false) + assert.NoError(t, err) + assert.NotNil(t, info) +} diff --git a/internal/collector/any_filesystem.go b/internal/collector/any_filesystem.go new file mode 100644 index 00000000..0b1f8036 --- /dev/null +++ b/internal/collector/any_filesystem.go @@ -0,0 +1,103 @@ +// Package collector is a pgSCV collectors +package collector + +import ( + "context" + "fmt" + "runtime" + + "github.com/cherts/pgscv/internal/filter" + "github.com/cherts/pgscv/internal/model" + "github.com/prometheus/client_golang/prometheus" + "github.com/shirou/gopsutil/v4/disk" +) + +type filesystemCollector struct { + bytes typedDesc + bytesTotal typedDesc + files typedDesc + filesTotal typedDesc +} + +// NewFilesystemCollector returns a new Collector exposing filesystem stats. +func NewFilesystemCollector(constLabels labels, settings model.CollectorSettings) (Collector, error) { + + // Define default filters (if no already present) to avoid collecting metrics about exotic filesystems. + if _, ok := settings.Filters["fstype"]; !ok { + if settings.Filters == nil { + settings.Filters = filter.New() + } + switch runtime.GOOS { + case "windows": + settings.Filters.Add("fstype", filter.Filter{Include: `^(NTFS|FAT32|exFAT)$`}) + case "darwin": + settings.Filters.Add("fstype", filter.Filter{Include: `^(apfs)$`}) + case "linux": + settings.Filters.Add("fstype", filter.Filter{Include: `^(ext3|ext4|xfs|btrfs)$`}) + case "freebsd", "openbsd": + settings.Filters.Add("fstype", filter.Filter{Include: `^(ufs|ufs2)$`}) + } + err := settings.Filters.Compile() + if err != nil { + return nil, err + } + } + + return &filesystemCollector{ + bytes: newBuiltinTypedDesc( + descOpts{"node", "filesystem", "bytes", "Number of bytes of filesystem by usage.", 0}, + prometheus.GaugeValue, + []string{"device", "mountpoint", "fstype", "usage"}, constLabels, + settings.Filters, + ), + bytesTotal: newBuiltinTypedDesc( + descOpts{"node", "filesystem", "bytes_total", "Total number of bytes of filesystem capacity.", 0}, + prometheus.GaugeValue, + []string{"device", "mountpoint", "fstype"}, constLabels, + settings.Filters, + ), + files: newBuiltinTypedDesc( + descOpts{"node", "filesystem", "files", "Number of files (inodes) of filesystem by usage.", 0}, + prometheus.GaugeValue, + []string{"device", "mountpoint", "fstype", "usage"}, constLabels, + settings.Filters, + ), + filesTotal: newBuiltinTypedDesc( + descOpts{"node", "filesystem", "files_total", "Total number of files (inodes) of filesystem capacity.", 0}, + prometheus.GaugeValue, + []string{"device", "mountpoint", "fstype"}, constLabels, + settings.Filters, + ), + }, nil +} + +// Update method collects filesystem usage statistics. +func (c *filesystemCollector) Update(_ context.Context, _ Config, ch chan<- prometheus.Metric) error { + diskStat, err := disk.Partitions(false) + if err != nil { + return fmt.Errorf("get filesystem stats failed: %s", err) + } + + for _, diskData := range diskStat { + + partitionStat, err := disk.Usage(diskData.Mountpoint) + if err != nil { + return fmt.Errorf("get partition stats failed: %s", err) + } + + // Truncate device paths to device names, e.g /dev/sda -> sda + device := truncateDeviceName(diskData.Device) + + // bytes; free = avail + reserved; total = used + free + ch <- c.bytesTotal.newConstMetric(float64(partitionStat.Total), device, diskData.Mountpoint, diskData.Fstype) + ch <- c.bytes.newConstMetric(float64(partitionStat.Total)-float64(partitionStat.Used), device, diskData.Mountpoint, diskData.Fstype, "avail") + //ch <- c.bytes.newConstMetric(s.free-s.avail, device, diskData.Mountpoint, diskData.Fstype, "reserved") + ch <- c.bytes.newConstMetric(float64(partitionStat.Used), device, diskData.Mountpoint, diskData.Fstype, "used") + // files (inodes) + ch <- c.filesTotal.newConstMetric(float64(partitionStat.InodesTotal), device, diskData.Mountpoint, diskData.Fstype) + ch <- c.files.newConstMetric(float64(partitionStat.InodesFree), device, diskData.Mountpoint, diskData.Fstype, "free") + ch <- c.files.newConstMetric(float64(partitionStat.InodesUsed), device, diskData.Mountpoint, diskData.Fstype, "used") + } + + return nil +} diff --git a/internal/collector/any_filesystem_test.go b/internal/collector/any_filesystem_test.go new file mode 100644 index 00000000..9da80d93 --- /dev/null +++ b/internal/collector/any_filesystem_test.go @@ -0,0 +1,37 @@ +package collector + +import ( + "runtime" + "testing" + + "github.com/cherts/pgscv/internal/filter" + "github.com/cherts/pgscv/internal/model" + "github.com/shirou/gopsutil/v4/disk" + "github.com/stretchr/testify/assert" +) + +func TestFilesystemCollector_Update(t *testing.T) { + var input = pipelineInput{ + required: []string{ + "node_filesystem_bytes", + "node_filesystem_bytes_total", + "node_filesystem_files", + "node_filesystem_files_total", + }, + collector: NewFilesystemCollector, + collectorSettings: model.CollectorSettings{Filters: filter.New()}, + } + + pipeline(t, input) +} + +func Test_getFilesystemStats(t *testing.T) { + path := "/" + if runtime.GOOS == "windows" { + path = "C:" + } + got, err := disk.Usage(path) + assert.NoError(t, err) + assert.NotNil(t, got) + assert.Equalf(t, got.Path, path, "error %v", err) +} diff --git a/internal/collector/linux_load_average.go b/internal/collector/any_load_average.go similarity index 52% rename from internal/collector/linux_load_average.go rename to internal/collector/any_load_average.go index 396d9a04..d478d4a6 100644 --- a/internal/collector/linux_load_average.go +++ b/internal/collector/any_load_average.go @@ -4,13 +4,10 @@ package collector import ( "context" "fmt" - "os" - "strconv" - "strings" - "github.com/cherts/pgscv/internal/log" "github.com/cherts/pgscv/internal/model" "github.com/prometheus/client_golang/prometheus" + "github.com/shirou/gopsutil/v4/load" ) type loadaverageCollector struct { @@ -45,44 +42,14 @@ func NewLoadAverageCollector(constLabels labels, settings model.CollectorSetting // Update implements Collector and exposes load average related metrics from /proc/loadavg. func (c *loadaverageCollector) Update(_ context.Context, _ Config, ch chan<- prometheus.Metric) error { - stats, err := getLoadAverageStats() + stats, err := load.Avg() if err != nil { - return fmt.Errorf("get load average stats failed: %s", err) + return fmt.Errorf("failed to get load average stats: %s", err) } - ch <- c.load1.newConstMetric(stats[0]) - ch <- c.load5.newConstMetric(stats[1]) - ch <- c.load15.newConstMetric(stats[2]) + ch <- c.load1.newConstMetric(stats.Load1) + ch <- c.load5.newConstMetric(stats.Load5) + ch <- c.load15.newConstMetric(stats.Load15) return nil } - -// getLoadAverageStats reads /proc/loadavg and return load stats. -func getLoadAverageStats() ([]float64, error) { - data, err := os.ReadFile("/proc/loadavg") - if err != nil { - return nil, err - } - - return parseLoadAverageStats(string(data)) -} - -// parseLoadAverageStats parses content from /proc/loadavg and return load stats. -func parseLoadAverageStats(data string) ([]float64, error) { - log.Debug("parse load average stats") - - parts := strings.Fields(data) - if len(parts) < 3 { - return nil, fmt.Errorf("invalid input, '%s': too few values", data) - } - - var err error - loads := make([]float64, 3) - for i, load := range parts[0:3] { - loads[i], err = strconv.ParseFloat(load, 64) - if err != nil { - return nil, fmt.Errorf("invalid input, parse '%s' failed: %w", load, err) - } - } - return loads, nil -} diff --git a/internal/collector/any_load_average_test.go b/internal/collector/any_load_average_test.go new file mode 100644 index 00000000..23d1fefc --- /dev/null +++ b/internal/collector/any_load_average_test.go @@ -0,0 +1,27 @@ +package collector + +import ( + "testing" + + "github.com/shirou/gopsutil/v4/load" + "github.com/stretchr/testify/assert" +) + +func TestLoadAverageCollector_Update(t *testing.T) { + var input = pipelineInput{ + required: []string{ + "node_load1", + "node_load5", + "node_load15", + }, + collector: NewLoadAverageCollector, + } + + pipeline(t, input) +} + +func Test_getLaInfo(t *testing.T) { + info, err := load.Avg() + assert.NoError(t, err) + assert.NotNil(t, info) +} diff --git a/internal/collector/linux_memory.go b/internal/collector/any_memory.go similarity index 51% rename from internal/collector/linux_memory.go rename to internal/collector/any_memory.go index c958d300..195f3d53 100644 --- a/internal/collector/linux_memory.go +++ b/internal/collector/any_memory.go @@ -4,10 +4,12 @@ package collector import ( "bufio" "context" + "encoding/json" "fmt" "io" "os" "regexp" + "runtime" "strconv" "strings" @@ -15,13 +17,13 @@ import ( "github.com/cherts/pgscv/internal/log" "github.com/cherts/pgscv/internal/model" "github.com/prometheus/client_golang/prometheus" + "github.com/shirou/gopsutil/v4/mem" ) type meminfoCollector struct { re *regexp.Regexp subsysFilters filter.Filters constLabels labels - memused typedDesc swapused typedDesc } @@ -31,12 +33,6 @@ func NewMeminfoCollector(constLabels labels, settings model.CollectorSettings) ( re: regexp.MustCompile(`\((.*)\)`), subsysFilters: settings.Filters, constLabels: constLabels, - memused: newBuiltinTypedDesc( - descOpts{"node", "memory", "MemUsed", "Memory information composite field MemUsed.", 0}, - prometheus.GaugeValue, - nil, constLabels, - settings.Filters, - ), swapused: newBuiltinTypedDesc( descOpts{"node", "memory", "SwapUsed", "Memory information composite field SwapUsed.", 0}, prometheus.GaugeValue, @@ -48,18 +44,23 @@ func NewMeminfoCollector(constLabels labels, settings model.CollectorSettings) ( // Update method collects network interfaces statistics. func (c *meminfoCollector) Update(_ context.Context, _ Config, ch chan<- prometheus.Metric) error { - meminfo, err := getMeminfoStats() + memInfo, err := mem.VirtualMemory() if err != nil { - return fmt.Errorf("get /proc/meminfo stats failed: %s", err) + return fmt.Errorf("failed to get VirtualMemory: %s", err) } - vmstat, err := getVmstatStats() + memInfoData, err := json.MarshalIndent(memInfo, "", "") if err != nil { - return fmt.Errorf("get /proc/vmstat stats failed: %s", err) + return fmt.Errorf("failed convert VirtualMemory data: %s", err) + } + + var memInfoJSONMap map[string]any + if err := json.Unmarshal([]byte(memInfoData), &memInfoJSONMap); err != nil { + return fmt.Errorf("failed to read VirtualMemory json data: %s", err) } - // Processing meminfo stats. - for param, value := range meminfo { + // Processing memInfoJSONMap stats. + for param, value := range memInfoJSONMap { param = c.re.ReplaceAllString(param, "_${1}") desc := newBuiltinTypedDesc( descOpts{"node", "memory", param, fmt.Sprintf("Memory information field %s.", param), 0}, @@ -68,80 +69,45 @@ func (c *meminfoCollector) Update(_ context.Context, _ Config, ch chan<- prometh c.subsysFilters, ) - ch <- desc.newConstMetric(value) + ch <- desc.newConstMetric(value.(float64)) } - // MemUsed and SwapUsed are composite metrics and not present in /proc/meminfo. - ch <- c.memused.newConstMetric(meminfo["MemTotal"] - meminfo["MemFree"] - meminfo["Buffers"] - meminfo["Cached"]) - ch <- c.swapused.newConstMetric(meminfo["SwapTotal"] - meminfo["SwapFree"]) - - // Processing vmstat stats. - for param, value := range vmstat { - // Depending on key name, make an assumption about metric type. - // Analyzing of vmstat content shows that gauge values have 'nr_' prefix. But without of - // strong knowledge of kernel internals this is just an assumption and could be mistaken. - t := prometheus.CounterValue - if strings.HasPrefix(param, "nr_") { - t = prometheus.GaugeValue - } - - param = c.re.ReplaceAllString(param, "_${1}") - - desc := newBuiltinTypedDesc( - descOpts{"node", "vmstat", param, fmt.Sprintf("Vmstat information field %s.", param), 0}, - t, nil, c.constLabels, c.subsysFilters, - ) - - ch <- desc.newConstMetric(value) - } - - return nil -} - -// getMeminfoStats is the intermediate function which opens stats file and run stats parser for extracting stats. -func getMeminfoStats() (map[string]float64, error) { - file, err := os.Open("/proc/meminfo") + swapInfo, err := mem.SwapMemory() if err != nil { - return nil, err + return fmt.Errorf("failed get SwapMemory: %s", err) } - defer func() { _ = file.Close() }() - - return parseMeminfoStats(file) -} -// parseMeminfoStats accepts file descriptor, reads file content and produces stats. -func parseMeminfoStats(r io.Reader) (map[string]float64, error) { - log.Debug("parse meminfo stats") - - var ( - scanner = bufio.NewScanner(r) - stats = map[string]float64{} - ) - - // Parse line by line, split line to param and value, parse the value to float and save to store. - for scanner.Scan() { - parts := strings.Fields(scanner.Text()) + // MemUsed and SwapUsed are composite metrics and not present in /proc/meminfo. + ch <- c.swapused.newConstMetric(float64(swapInfo.Total) - float64(swapInfo.Free)) - if len(parts) < 2 || len(parts) > 3 { - return nil, fmt.Errorf("invalid input, '%s': wrong number of values", scanner.Text()) + if runtime.GOOS == "linux" { + vmstat, err := getVmstatStats() + if err != nil { + return fmt.Errorf("get /proc/vmstat stats failed: %s", err) } - param, value := strings.TrimRight(parts[0], ":"), parts[1] + // Processing vmstat stats. + for param, value := range vmstat { + // Depending on key name, make an assumption about metric type. + // Analyzing of vmstat content shows that gauge values have 'nr_' prefix. But without of + // strong knowledge of kernel internals this is just an assumption and could be mistaken. + t := prometheus.CounterValue + if strings.HasPrefix(param, "nr_") { + t = prometheus.GaugeValue + } - v, err := strconv.ParseFloat(value, 64) - if err != nil { - log.Errorf("invalid input, parse '%s' failed: %s, skip", value, err.Error()) - continue - } + param = c.re.ReplaceAllString(param, "_${1}") - if len(parts) == 3 && parts[2] == "kB" { - v *= 1024 - } + desc := newBuiltinTypedDesc( + descOpts{"node", "vmstat", param, fmt.Sprintf("Vmstat information field %s.", param), 0}, + t, nil, c.constLabels, c.subsysFilters, + ) - stats[param] = v + ch <- desc.newConstMetric(value) + } } - return stats, scanner.Err() + return nil } // getVmstatStats is the intermediate function which opens stats file and run stats parser for extracting stats. diff --git a/internal/collector/any_memory_test.go b/internal/collector/any_memory_test.go new file mode 100644 index 00000000..5c789448 --- /dev/null +++ b/internal/collector/any_memory_test.go @@ -0,0 +1,184 @@ +package collector + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/shirou/gopsutil/v4/mem" + "github.com/stretchr/testify/assert" +) + +func TestMeminfoCollector_Update(t *testing.T) { + vInfo, err := mem.VirtualMemory() + assert.NoError(t, err) + assert.NotNil(t, vInfo) + + fmt.Printf("Total: %v, Free: %v, UsedPercent: %f%%\n", vInfo.Total, vInfo.Free, vInfo.UsedPercent) + fmt.Printf("SwapTotal: %d, SwapFree: %d, SwapCached: %d\n", vInfo.SwapTotal, vInfo.SwapFree, vInfo.SwapCached) + data, _ := json.MarshalIndent(vInfo, "", "") + var jsonMap map[string]any + if err := json.Unmarshal([]byte(data), &jsonMap); err != nil { + fmt.Println("Error reading JSON:", err) + } + for param, value := range jsonMap { + fmt.Printf("%s - %f\n", param, value.(float64)) + } + + sInfo, err := mem.SwapMemory() + assert.NoError(t, err) + assert.NotNil(t, sInfo) + + if runtime.GOOS == "linux" { + vmstat, err := os.ReadFile("/proc/vmstat") + assert.NoError(t, err) + fmt.Println(string(vmstat)) + + var input = pipelineInput{ + required: []string{ + // memInfo + "node_memory_total", "node_memory_free", "node_memory_available", "node_memory_active", + "node_memory_buffers", "node_memory_cached", "node_memory_active", + "node_memory_swapCached", "node_memory_swapTotal", "node_memory_swapFree", + // vmstat + "node_vmstat_nr_anon_pages", "node_vmstat_nr_mapped", "node_vmstat_nr_dirty", "node_vmstat_nr_writeback", + "node_vmstat_pgpgin", "node_vmstat_pgpgout", "node_vmstat_pswpin", "node_vmstat_pswpout", + }, + optional: []string{ + // memInfo + "node_memory_sreclaimable", "node_memory_slab", "node_memory_vmallocUsed", "node_memory_hugePagesFree", + "node_memory_lowFree", "node_memory_mapped", "node_memory_laundry", "node_memory_dirty", + "node_memory_shared", "node_memory_used", "node_memory_writeBack", "node_memory_highFree", + "node_memory_hugePagesTotal", "node_memory_vmallocTotal", "node_memory_hugePageSize", + "node_memory_wired", "node_memory_committedAS", "node_memory_vmallocChunk", + "node_memory_hugePagesRsvd", "node_memory_usedPercent", "node_memory_writeBackTmp", + "node_memory_sunreclaim", "node_memory_highTotal", "node_memory_hugePagesSurp", + "node_memory_anonHugePages", "node_memory_pageTables", "node_memory_inactive", + "node_memory_commitLimit", "node_memory_lowTotal", "node_memory_SwapUsed", + // vmstat + "node_vmstat_nr_free_pages", "node_vmstat_nr_zone_inactive_anon", "node_vmstat_nr_zone_active_anon", + "node_vmstat_nr_zone_inactive_file", "node_vmstat_nr_zone_active_file", "node_vmstat_nr_zone_unevictable", + "node_vmstat_nr_zone_write_pending", "node_vmstat_nr_mlock", "node_vmstat_nr_page_table_pages", + "node_vmstat_nr_kernel_stack", "node_vmstat_nr_bounce", "node_vmstat_nr_zspages", "node_vmstat_nr_free_cma", + "node_vmstat_numa_hit", "node_vmstat_numa_miss", "node_vmstat_numa_foreign", "node_vmstat_numa_interleave", + "node_vmstat_numa_local", "node_vmstat_numa_other", "node_vmstat_nr_inactive_anon", "node_vmstat_nr_active_anon", + "node_vmstat_nr_inactive_file", "node_vmstat_nr_active_file", "node_vmstat_nr_unevictable", + "node_vmstat_nr_slab_reclaimable", "node_vmstat_nr_slab_unreclaimable", "node_vmstat_nr_isolated_anon", + "node_vmstat_nr_isolated_file", "node_vmstat_workingset_nodes", "node_vmstat_workingset_refault", + "node_vmstat_workingset_activate", "node_vmstat_workingset_restore", "node_vmstat_workingset_nodereclaim", + "node_vmstat_nr_file_pages", "node_vmstat_nr_writeback_temp", "node_vmstat_nr_shmem", + "node_vmstat_nr_shmem_hugepages", "node_vmstat_nr_shmem_pmdmapped", "node_vmstat_nr_file_hugepages", + "node_vmstat_nr_file_pmdmapped", "node_vmstat_nr_anon_transparent_hugepages", "node_vmstat_nr_unstable", + "node_vmstat_nr_vmscan_write", "node_vmstat_nr_vmscan_immediate_reclaim", "node_vmstat_nr_dirtied", + "node_vmstat_nr_written", "node_vmstat_nr_kernel_misc_reclaimable", "node_vmstat_nr_dirty_threshold", + "node_vmstat_nr_dirty_background_threshold", "node_vmstat_pgalloc_dma", "node_vmstat_pgalloc_dma32", + "node_vmstat_pgalloc_normal", "node_vmstat_pgalloc_movable", "node_vmstat_allocstall_dma", + "node_vmstat_allocstall_dma32", "node_vmstat_allocstall_normal", "node_vmstat_allocstall_movable", + "node_vmstat_pgskip_dma", "node_vmstat_pgskip_dma32", "node_vmstat_pgskip_normal", + "node_vmstat_pgskip_movable", "node_vmstat_pgfree", "node_vmstat_pgactivate", "node_vmstat_pgdeactivate", + "node_vmstat_pglazyfree", "node_vmstat_pgfault", "node_vmstat_pgmajfault", "node_vmstat_pglazyfreed", + "node_vmstat_pgrefill", "node_vmstat_pgsteal_kswapd", "node_vmstat_pgsteal_direct", + "node_vmstat_pgscan_kswapd", "node_vmstat_pgscan_direct", "node_vmstat_pgscan_direct_throttle", + "node_vmstat_zone_reclaim_failed", "node_vmstat_pginodesteal", "node_vmstat_slabs_scanned", + "node_vmstat_kswapd_inodesteal", "node_vmstat_kswapd_low_wmark_hit_quickly", + "node_vmstat_kswapd_high_wmark_hit_quickly", "node_vmstat_pageoutrun", "node_vmstat_pgrotated", + "node_vmstat_drop_pagecache", "node_vmstat_drop_slab", "node_vmstat_oom_kill", "node_vmstat_numa_pte_updates", + "node_vmstat_numa_huge_pte_updates", "node_vmstat_numa_hint_faults", "node_vmstat_numa_hint_faults_local", + "node_vmstat_numa_pages_migrated", "node_vmstat_pgmigrate_success", "node_vmstat_pgmigrate_fail", + "node_vmstat_compact_migrate_scanned", "node_vmstat_compact_free_scanned", "node_vmstat_compact_isolated", + "node_vmstat_compact_stall", "node_vmstat_compact_fail", "node_vmstat_compact_success", + "node_vmstat_compact_daemon_wake", "node_vmstat_compact_daemon_migrate_scanned", + "node_vmstat_compact_daemon_free_scanned", "node_vmstat_htlb_buddy_alloc_success", + "node_vmstat_htlb_buddy_alloc_fail", "node_vmstat_unevictable_pgs_culled", + "node_vmstat_unevictable_pgs_scanned", "node_vmstat_unevictable_pgs_rescued", + "node_vmstat_unevictable_pgs_mlocked", "node_vmstat_unevictable_pgs_munlocked", + "node_vmstat_unevictable_pgs_cleared", "node_vmstat_unevictable_pgs_stranded", + "node_vmstat_thp_fault_alloc", "node_vmstat_thp_fault_fallback", "node_vmstat_thp_collapse_alloc", + "node_vmstat_thp_collapse_alloc_failed", "node_vmstat_thp_file_alloc", "node_vmstat_thp_file_mapped", + "node_vmstat_thp_split_page", "node_vmstat_thp_split_page_failed", "node_vmstat_thp_deferred_split_page", + "node_vmstat_thp_split_pmd", "node_vmstat_thp_split_pud", "node_vmstat_thp_zero_page_alloc", + "node_vmstat_thp_zero_page_alloc_failed", "node_vmstat_thp_swpout", "node_vmstat_thp_swpout_fallback", + "node_vmstat_thp_migration_fail", "node_vmstat_workingset_restore_anon", "node_vmstat_workingset_activate_file", + "node_vmstat_workingset_refault_file", "node_vmstat_thp_migration_split", "node_vmstat_workingset_refault_anon", + "node_vmstat_pgreuse", "node_vmstat_thp_migration_success", "node_vmstat_workingset_activate_anon", + "node_vmstat_workingset_restore_file", "node_vmstat_balloon_inflate", "node_vmstat_balloon_deflate", + "node_vmstat_balloon_migrate", "node_vmstat_swap_ra", "node_vmstat_swap_ra_hit", "node_vmstat_nr_foll_pin_acquired", + "node_vmstat_pgsteal_anon", "node_vmstat_pgsteal_file", "node_vmstat_pgscan_file", "node_vmstat_pgscan_anon", + "node_vmstat_thp_file_fallback_charge", "node_vmstat_nr_foll_pin_released", "node_vmstat_thp_file_fallback", + "node_vmstat_thp_fault_fallback_charge", "node_vmstat_nr_swapcached", "node_vmstat_direct_map_level2_splits", + "node_vmstat_direct_map_level3_splits", "node_vmstat_workingset_refault", "node_vmstat_workingset_activate", + "node_vmstat_workingset_restore", "node_vmstat_pgdemote_kswapd", "node_vmstat_pgdemote_direct", + "node_vmstat_pgsteal_khugepaged", "node_vmstat_pgskip_device", "node_vmstat_pgdemote_khugepaged", + "node_vmstat_thp_scan_exceed_none_pte", "node_vmstat_nr_throttled_written", "node_vmstat_thp_scan_exceed_share_pte", + "node_vmstat_pgpromote_candidate", "node_vmstat_pgscan_khugepaged", "node_vmstat_zswpin", + "node_vmstat_thp_scan_exceed_swap_pte", "node_vmstat_allocstall_device", "node_vmstat_pgpromote_success", + "node_vmstat_pgalloc_device", "node_vmstat_nr_sec_page_table_pages", "node_vmstat_cow_ksm", + "node_vmstat_ksm_swpin_copy", "node_vmstat_zswpout", "node_vmstat_nr_unaccepted", "node_vmstat_zswpwb", + }, + collector: NewMeminfoCollector, + } + + pipeline(t, input) + } +} + +func Test_getVmstatStats(t *testing.T) { + if runtime.GOOS == "linux" { + s, err := getVmstatStats() + assert.NoError(t, err) + assert.Greater(t, len(s), 0) + } +} + +func Test_parseVmstatStats(t *testing.T) { + file, err := os.Open(filepath.Clean("testdata/proc/vmstat.golden")) + assert.NoError(t, err) + + stats, err := parseVmstatStats(file) + assert.NoError(t, err) + + wantStats := map[string]float64{ + "oom_kill": 10, + "nr_zone_active_file": 1933629, + "nr_unevictable": 24, + "nr_writeback": 0, + "pgactivate": 57995375, + } + + for k, want := range wantStats { + if got, ok := stats[k]; ok { + assert.Equal(t, want, got) + } else { + assert.Fail(t, "not found") + } + } + + assert.NoError(t, file.Close()) + + // test with invalid values + file, err = os.Open(filepath.Clean("testdata/proc/vmstat.invalid.1")) + assert.NoError(t, err) + stats, err = parseVmstatStats(file) + assert.NoError(t, err) + assert.Equal(t, 3, len(stats)) + assert.NoError(t, file.Close()) + + // test with wrong number of fields + file, err = os.Open(filepath.Clean("testdata/proc/vmstat.invalid.2")) + assert.NoError(t, err) + _, err = parseVmstatStats(file) + assert.Error(t, err) + assert.NoError(t, file.Close()) + + // test with wrong format file + file, err = os.Open(filepath.Clean("testdata/proc/netdev.golden")) + assert.NoError(t, err) + + stats, err = parseVmstatStats(file) + assert.Error(t, err) + assert.Nil(t, stats) + assert.NoError(t, file.Close()) +} diff --git a/internal/collector/any_netdev.go b/internal/collector/any_netdev.go new file mode 100644 index 00000000..7b889e8c --- /dev/null +++ b/internal/collector/any_netdev.go @@ -0,0 +1,82 @@ +// Package collector is a pgSCV collectors +package collector + +import ( + "context" + "fmt" + + "github.com/cherts/pgscv/internal/filter" + "github.com/cherts/pgscv/internal/model" + "github.com/prometheus/client_golang/prometheus" + "github.com/shirou/gopsutil/v4/net" +) + +type netdevCollector struct { + bytes typedDesc + packets typedDesc + events typedDesc +} + +// NewNetdevCollector returns a new Collector exposing network interfaces stats. +func NewNetdevCollector(constLabels labels, settings model.CollectorSettings) (Collector, error) { + + // Define default filters (if no already present) to avoid collecting metrics about virtual interfaces. + if _, ok := settings.Filters["device"]; !ok { + if settings.Filters == nil { + settings.Filters = filter.New() + } + + settings.Filters.Add("device", filter.Filter{Exclude: `docker|virbr`}) + err := settings.Filters.Compile() + if err != nil { + return nil, err + } + } + + return &netdevCollector{ + bytes: newBuiltinTypedDesc( + descOpts{"node", "network", "bytes_total", "Total number of bytes processed by network device, by each direction.", 0}, + prometheus.CounterValue, + []string{"device", "type"}, constLabels, + settings.Filters, + ), + packets: newBuiltinTypedDesc( + descOpts{"node", "network", "packets_total", "Total number of packets processed by network device, by each direction.", 0}, + prometheus.CounterValue, + []string{"device", "type"}, constLabels, + settings.Filters, + ), + events: newBuiltinTypedDesc( + descOpts{"node", "network", "events_total", "Total number of events occurred on network device, by each type and direction.", 0}, + prometheus.CounterValue, + []string{"device", "type", "event"}, constLabels, + settings.Filters, + ), + }, nil +} + +// Update method collects network interfaces statistics +func (c *netdevCollector) Update(_ context.Context, _ Config, ch chan<- prometheus.Metric) error { + netStat, err := net.IOCounters(true) + if err != nil { + return fmt.Errorf("Failed to get netdev stats: %s", err) + } + + for _, stat := range netStat { + // recv + ch <- c.bytes.newConstMetric(float64(stat.BytesRecv), stat.Name, "recv") + ch <- c.packets.newConstMetric(float64(stat.PacketsRecv), stat.Name, "recv") + ch <- c.events.newConstMetric(float64(stat.Errin), stat.Name, "recv", "errs") + ch <- c.events.newConstMetric(float64(stat.Dropin), stat.Name, "recv", "drop") + ch <- c.events.newConstMetric(float64(stat.Fifoin), stat.Name, "recv", "fifo") + + // sent + ch <- c.bytes.newConstMetric(float64(stat.BytesSent), stat.Name, "sent") + ch <- c.packets.newConstMetric(float64(stat.PacketsSent), stat.Name, "sent") + ch <- c.events.newConstMetric(float64(stat.Errout), stat.Name, "sent", "errs") + ch <- c.events.newConstMetric(float64(stat.Dropout), stat.Name, "sent", "drop") + ch <- c.events.newConstMetric(float64(stat.Fifoout), stat.Name, "sent", "fifo") + } + + return nil +} diff --git a/internal/collector/any_netdev_test.go b/internal/collector/any_netdev_test.go new file mode 100644 index 00000000..748d3ab1 --- /dev/null +++ b/internal/collector/any_netdev_test.go @@ -0,0 +1,30 @@ +package collector + +import ( + "testing" + + "github.com/cherts/pgscv/internal/filter" + "github.com/cherts/pgscv/internal/model" + "github.com/shirou/gopsutil/v4/net" + "github.com/stretchr/testify/assert" +) + +func TestNetdevCollector_Update(t *testing.T) { + var input = pipelineInput{ + required: []string{ + "node_network_bytes_total", + "node_network_packets_total", + "node_network_events_total", + }, + collector: NewNetdevCollector, + collectorSettings: model.CollectorSettings{Filters: filter.New()}, + } + + pipeline(t, input) +} + +func Test_getNetdevStats(t *testing.T) { + info, err := net.IOCounters(true) + assert.NoError(t, err) + assert.NotNil(t, info) +} diff --git a/internal/collector/linux_network.go b/internal/collector/any_network.go similarity index 100% rename from internal/collector/linux_network.go rename to internal/collector/any_network.go diff --git a/internal/collector/linux_network_test.go b/internal/collector/any_network_test.go similarity index 100% rename from internal/collector/linux_network_test.go rename to internal/collector/any_network_test.go diff --git a/internal/collector/any_sysinfo.go b/internal/collector/any_sysinfo.go new file mode 100644 index 00000000..4eb4d4c8 --- /dev/null +++ b/internal/collector/any_sysinfo.go @@ -0,0 +1,70 @@ +package collector + +import ( + "context" + + "github.com/cherts/pgscv/internal/model" + "github.com/prometheus/client_golang/prometheus" + "github.com/shirou/gopsutil/v4/host" +) + +type sysinfoCollector struct { + platform typedDesc + os typedDesc + host typedDesc + uptime typedDesc + boottime typedDesc +} + +// NewSysInfoCollector returns a new Collector exposing system info. +func NewSysInfoCollector(constLabels labels, settings model.CollectorSettings) (Collector, error) { + return &sysinfoCollector{ + platform: newBuiltinTypedDesc( + descOpts{"node", "platform", "info", "Labeled system platform information", 0}, + prometheus.GaugeValue, + []string{"virtualization_role", "virtualization_system"}, constLabels, + settings.Filters, + ), + os: newBuiltinTypedDesc( + descOpts{"node", "os", "info", "Labeled operating system information.", 0}, + prometheus.GaugeValue, + []string{"type", "name", "family", "version", "kernel_arch", "kernel_version"}, constLabels, + settings.Filters, + ), + host: newBuiltinTypedDesc( + descOpts{"node", "host", "info", "Labeled host information.", 0}, + prometheus.GaugeValue, + []string{"hostname", "hostid"}, constLabels, + settings.Filters, + ), + uptime: newBuiltinTypedDesc( + descOpts{"node", "uptime", "seconds", "Total number of seconds the system has been up.", 0}, + prometheus.CounterValue, + nil, constLabels, + settings.Filters, + ), + boottime: newBuiltinTypedDesc( + descOpts{"node", "boottime", "seconds", "Node boot time, in unixtime.", 0}, + prometheus.GaugeValue, + nil, constLabels, + settings.Filters, + ), + }, nil +} + +// Update implements Collector and exposes system info metrics. +func (c *sysinfoCollector) Update(_ context.Context, _ Config, ch chan<- prometheus.Metric) error { + info, err := host.Info() + if err != nil { + return err + } + + ch <- c.platform.newConstMetric(1, info.VirtualizationRole, info.VirtualizationSystem) + ch <- c.os.newConstMetric(1, info.OS, info.Platform, info.PlatformFamily, info.PlatformVersion, info.KernelArch, info.KernelVersion) + ch <- c.host.newConstMetric(1, info.Hostname, info.HostID) + + ch <- c.uptime.newConstMetric(float64(info.Uptime)) + ch <- c.boottime.newConstMetric(float64(info.BootTime)) + + return nil +} diff --git a/internal/collector/linux_sysinfo_test.go b/internal/collector/any_sysinfo_test.go similarity index 52% rename from internal/collector/linux_sysinfo_test.go rename to internal/collector/any_sysinfo_test.go index 8c112f63..715f7f6c 100644 --- a/internal/collector/linux_sysinfo_test.go +++ b/internal/collector/any_sysinfo_test.go @@ -1,16 +1,18 @@ package collector import ( - "github.com/stretchr/testify/assert" - "os" - "path/filepath" "testing" + + "github.com/shirou/gopsutil/v4/host" + "github.com/stretchr/testify/assert" ) func TestSysInfoCollector_Update(t *testing.T) { var input = pipelineInput{ required: []string{ "node_platform_info", "node_os_info", + "node_host_info", "node_boottime_seconds", + "node_uptime_seconds", }, optional: []string{}, collector: NewSysInfoCollector, @@ -20,18 +22,7 @@ func TestSysInfoCollector_Update(t *testing.T) { } func Test_getSysInfo(t *testing.T) { - info, err := getSysInfo() + info, err := host.Info() assert.NoError(t, err) assert.NotNil(t, info) } - -func Test_parseOsRelease(t *testing.T) { - file, err := os.Open(filepath.Clean("testdata/etc/os-release.golden")) - assert.NoError(t, err) - defer func() { _ = file.Close() }() - - name, version, err := parseOsRelease(file) - assert.NoError(t, err) - assert.NotEqual(t, "", name) - assert.NotEqual(t, "", version) -} diff --git a/internal/collector/collector.go b/internal/collector/collector.go index 3f3b19b7..d92882d8 100644 --- a/internal/collector/collector.go +++ b/internal/collector/collector.go @@ -210,14 +210,23 @@ type PgscvCollector struct { // NewPgscvCollector accepts Factories and creates per-service instance of Collector. func NewPgscvCollector(serviceID string, factories Factories, config Config) (*PgscvCollector, error) { collectors := make(map[string]Collector) - pgConfig, err := pgx.ParseConfig(config.ConnString) - if err != nil { - return nil, err + + constLabels := make(labels) + constLabels["service_id"] = serviceID + + if config.ServiceType == model.ServiceTypePostgresql || config.ServiceType == model.ServiceTypePgbouncer { + pgConfig, err := pgx.ParseConfig(config.ConnString) + if err != nil { + return nil, err + } + constLabels["host"] = pgConfig.Host + constLabels["port"] = strconv.FormatUint(uint64(pgConfig.Port), 10) } - constLabels := labels{"service_id": serviceID, "host": pgConfig.Host, "port": strconv.FormatUint(uint64(pgConfig.Port), 10)} + if config.ConstLabels != nil { maps.Copy(constLabels, *config.ConstLabels) } + for key := range factories { settings := config.Settings[key] diff --git a/internal/collector/filesystem_common.go b/internal/collector/filesystem_common.go index 98c70450..4c95475f 100644 --- a/internal/collector/filesystem_common.go +++ b/internal/collector/filesystem_common.go @@ -2,13 +2,11 @@ package collector import ( - "bufio" - "fmt" - "io" "os" "strings" "github.com/cherts/pgscv/internal/log" + "github.com/shirou/gopsutil/v4/disk" ) // mount describes properties of mounted filesystems @@ -16,36 +14,28 @@ type mount struct { device string mountpoint string fstype string - options string + options []string } -// parseProcMounts parses /proc/mounts and returns slice of mounted filesystems properties. -func parseProcMounts(r io.Reader) ([]mount, error) { +// parseMounts parses disk partition info and returns slice of mounted filesystems properties. +func parseMounts(r []disk.PartitionStat) ([]mount, error) { log.Debug("parse mounted filesystems") var ( - scanner = bufio.NewScanner(r) - mounts []mount + mounts []mount ) // Parse line by line, split line to param and value, parse the value to float and save to store. - for scanner.Scan() { - parts := strings.Fields(scanner.Text()) - - if len(parts) != 6 { - return nil, fmt.Errorf("invalid input: '%s', skip", scanner.Text()) - } - + for _, diskData := range r { s := mount{ - device: parts[0], - mountpoint: parts[1], - fstype: parts[2], - options: parts[3], + device: diskData.Device, + mountpoint: diskData.Mountpoint, + fstype: diskData.Fstype, + options: diskData.Opts, } - mounts = append(mounts, s) } - return mounts, scanner.Err() + return mounts, nil } // truncateDeviceName truncates passed full path to device to short device name. diff --git a/internal/collector/filesystem_common_test.go b/internal/collector/filesystem_common_test.go index 7e80a471..2104a96c 100644 --- a/internal/collector/filesystem_common_test.go +++ b/internal/collector/filesystem_common_test.go @@ -1,37 +1,27 @@ package collector import ( - "github.com/stretchr/testify/assert" - "os" - "path/filepath" "testing" + + "github.com/shirou/gopsutil/v4/disk" + "github.com/stretchr/testify/assert" ) -func Test_parseProcMounts(t *testing.T) { - file, err := os.Open(filepath.Clean("testdata/proc/mounts.golden")) - assert.NoError(t, err) - defer func() { _ = file.Close() }() +func Test_parseMounts(t *testing.T) { + diskStat := []disk.PartitionStat{ + {Device: "/dev/vda1", Mountpoint: "/", Fstype: "ext4", Opts: []string{"rw", "relatime"}}, + {Device: "/dev/vda2", Mountpoint: "/var/lib/postgres", Fstype: "ext4", Opts: []string{"rw", "relatime"}}, + } - stats, err := parseProcMounts(file) + stats, err := parseMounts(diskStat) assert.NoError(t, err) want := []mount{ - {device: "/dev/mapper/ssd-root", mountpoint: "/", fstype: "ext4", options: "rw,relatime,discard,errors=remount-ro"}, - {device: "/dev/sda1", mountpoint: "/boot", fstype: "ext3", options: "rw,relatime"}, - {device: "/dev/mapper/ssd-data", mountpoint: "/data", fstype: "ext4", options: "rw,relatime,discard"}, - {device: "/dev/sdc1", mountpoint: "/archive", fstype: "xfs", options: "rw,relatime"}, + {device: "/dev/vda1", mountpoint: "/", fstype: "ext4", options: []string{"rw", "relatime"}}, + {device: "/dev/vda2", mountpoint: "/var/lib/postgres", fstype: "ext4", options: []string{"rw", "relatime"}}, } assert.Equal(t, want, stats) - - // test with wrong format file - file, err = os.Open(filepath.Clean("testdata/proc/netdev.golden")) - assert.NoError(t, err) - defer func() { _ = file.Close() }() - - stats, err = parseProcMounts(file) - assert.Error(t, err) - assert.Nil(t, stats) } func Test_truncateDeviceName(t *testing.T) { diff --git a/internal/collector/linux_cpu.go b/internal/collector/linux_cpu.go deleted file mode 100644 index ba1707e3..00000000 --- a/internal/collector/linux_cpu.go +++ /dev/null @@ -1,216 +0,0 @@ -// Package collector is a pgSCV collectors -package collector - -import ( - "bufio" - "bytes" - "context" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - - "github.com/cherts/pgscv/internal/log" - "github.com/cherts/pgscv/internal/model" - "github.com/prometheus/client_golang/prometheus" -) - -type cpuCollector struct { - systicks float64 - cpu typedDesc - cpuAll typedDesc - cpuGuest typedDesc - uptime typedDesc - idletime typedDesc -} - -// NewCPUCollector returns a new Collector exposing kernel/system statistics. -func NewCPUCollector(constLabels labels, settings model.CollectorSettings) (Collector, error) { - cmdOutput, err := exec.Command("getconf", "CLK_TCK").Output() - if err != nil { - return nil, fmt.Errorf("determine clock frequency failed: %s", err) - } - - value := strings.TrimSpace(string(cmdOutput)) - systicks, err := strconv.ParseFloat(value, 64) - if err != nil { - return nil, fmt.Errorf("invalid input: parse '%s' failed: %w", value, err) - } - - c := &cpuCollector{ - systicks: systicks, - cpu: newBuiltinTypedDesc( - descOpts{"node", "cpu", "seconds_total", "Seconds the CPUs spent in each mode.", 0}, - prometheus.CounterValue, - []string{"mode"}, constLabels, - settings.Filters, - ), - cpuAll: newBuiltinTypedDesc( - descOpts{"node", "cpu", "seconds_all_total", "Seconds the CPUs spent in all modes.", 0}, - prometheus.CounterValue, - nil, constLabels, - settings.Filters, - ), - cpuGuest: newBuiltinTypedDesc( - descOpts{"node", "cpu", "guest_seconds_total", "Seconds the CPUs spent in guests (VMs) for each mode.", 0}, - prometheus.CounterValue, - []string{"mode"}, constLabels, - settings.Filters, - ), - uptime: newBuiltinTypedDesc( - descOpts{"node", "uptime", "up_seconds_total", "Total number of seconds the system has been up, accordingly to /proc/uptime.", 0}, - prometheus.CounterValue, - nil, constLabels, - settings.Filters, - ), - idletime: newBuiltinTypedDesc( - descOpts{"node", "uptime", "idle_seconds_total", "Total number of seconds all cores have spent idle, accordingly to /proc/uptime.", 0}, - prometheus.CounterValue, - nil, constLabels, - settings.Filters, - ), - } - return c, nil -} - -// Update implements Collector and exposes cpu related metrics from /proc/stat and /sys/.../cpu/. -func (c *cpuCollector) Update(_ context.Context, _ Config, ch chan<- prometheus.Metric) error { - stat, err := getCPUStat(c.systicks) - if err != nil { - return fmt.Errorf("collect cpu usage stats failed: %s; skip", err) - } - - uptime, idletime, err := getProcUptime("/proc/uptime") - if err != nil { - return fmt.Errorf("collect uptime stats failed: %s; skip", err) - } - - // Collected time represents summary time spent by ALL cpu cores. - ch <- c.cpu.newConstMetric(stat.user, "user") - ch <- c.cpu.newConstMetric(stat.nice, "nice") - ch <- c.cpu.newConstMetric(stat.system, "system") - ch <- c.cpu.newConstMetric(stat.idle, "idle") - ch <- c.cpu.newConstMetric(stat.iowait, "iowait") - ch <- c.cpu.newConstMetric(stat.irq, "irq") - ch <- c.cpu.newConstMetric(stat.softirq, "softirq") - ch <- c.cpu.newConstMetric(stat.steal, "steal") - - ch <- c.cpuAll.newConstMetric(stat.user + stat.nice + stat.system + stat.idle + stat.iowait + stat.irq + stat.softirq + stat.steal) - - // Guest CPU is also accounted for in stat.user and stat.nice, expose these as separate metrics. - ch <- c.cpuGuest.newConstMetric(stat.guest, "user") - ch <- c.cpuGuest.newConstMetric(stat.guestnice, "nice") - - // Up and idle time values from /proc/uptime. Idle time accounted as summary for all cpu cores. - ch <- c.uptime.newConstMetric(uptime) - ch <- c.idletime.newConstMetric(idletime) - - return nil -} - -// systemProcStatCPU ... -type cpuStat struct { - user float64 - nice float64 - system float64 - idle float64 - iowait float64 - irq float64 - softirq float64 - steal float64 - guest float64 - guestnice float64 -} - -// getCPUStat opens stat file and executes parser. -func getCPUStat(systicks float64) (cpuStat, error) { - file, err := os.Open("/proc/stat") - if err != nil { - return cpuStat{}, err - } - defer func() { _ = file.Close() }() - - return parseProcCPUStat(file, systicks) -} - -// parseProcCPUStat parses stat file and returns total CPU usage stat. -func parseProcCPUStat(r io.Reader, systicks float64) (cpuStat, error) { - log.Debug("parse CPU stats") - - var scanner = bufio.NewScanner(r) - - for scanner.Scan() { - parts := strings.Fields(scanner.Text()) - if len(parts) < 2 { - log.Debug("CPU stat invalid input: too few values; skip") - continue - } - - // Looking only for total stat. We're not interested in per-CPU stats. - if parts[0] != "cpu" { - continue - } - - return parseCPUStat(scanner.Text(), systicks) - } - - return cpuStat{}, fmt.Errorf("total cpu stats not found") -} - -// parseCPUStat parses single line from stats file and returns parsed stats. -func parseCPUStat(line string, systicks float64) (cpuStat, error) { - s := cpuStat{} - var cpu string - - count, err := fmt.Sscanf( - line, - "%s %f %f %f %f %f %f %f %f %f %f", - &cpu, &s.user, &s.nice, &s.system, &s.idle, &s.iowait, &s.irq, &s.softirq, &s.steal, &s.guest, &s.guestnice, - ) - - if err != nil && err != io.EOF { - return cpuStat{}, fmt.Errorf("invalid input, parse '%s' failed: %w", line, err) - } - if count != 11 { - return cpuStat{}, fmt.Errorf("invalid input, parse '%s' failed: wrong number of values", line) - } - - s.user /= systicks - s.nice /= systicks - s.system /= systicks - s.idle /= systicks - s.iowait /= systicks - s.irq /= systicks - s.softirq /= systicks - s.steal /= systicks - s.guest /= systicks - s.guestnice /= systicks - - return s, nil -} - -// getProcUptime parses uptime file (e.g. /proc/uptime) and return uptime and idletime values. -func getProcUptime(procfile string) (float64, float64, error) { - content, err := os.ReadFile(filepath.Clean(procfile)) - if err != nil { - return 0, 0, err - } - - reader := bufio.NewReader(bytes.NewBuffer(content)) - - line, _, err := reader.ReadLine() - if err != nil { - return 0, 0, err - } - - var up, idle float64 - _, err = fmt.Sscanf(string(line), "%f %f", &up, &idle) - if err != nil { - return 0, 0, err - } - - return up, idle, nil -} diff --git a/internal/collector/linux_cpu_test.go b/internal/collector/linux_cpu_test.go deleted file mode 100644 index 2a516a14..00000000 --- a/internal/collector/linux_cpu_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package collector - -import ( - "github.com/stretchr/testify/assert" - "os" - "path/filepath" - "testing" -) - -func TestCPUCollector_Update(t *testing.T) { - var input = pipelineInput{ - required: []string{ - "node_cpu_seconds_total", - "node_cpu_seconds_all_total", - "node_cpu_guest_seconds_total", - "node_uptime_up_seconds_total", - "node_uptime_idle_seconds_total", - }, - collector: NewCPUCollector, - } - - pipeline(t, input) -} - -func Test_parseProcCPUStat(t *testing.T) { - testcases := []struct { - in string - valid bool - want cpuStat - }{ - {in: "testdata/proc/stat.golden", valid: true, want: cpuStat{ - user: 30976.68, - nice: 15.93, - system: 14196.18, - idle: 1322422.58, - iowait: 425.35, - irq: 0, - softirq: 3846.86, - steal: 0, - guest: 0, - guestnice: 0, - }}, - {in: "testdata/proc/stat.invalid", valid: false}, - } - - for _, tc := range testcases { - file, err := os.Open(filepath.Clean(tc.in)) - assert.NoError(t, err) - - got, err := parseProcCPUStat(file, 100) - if tc.valid { - assert.NoError(t, err) - assert.Equal(t, tc.want, got) - } else { - assert.Error(t, err) - } - assert.NoError(t, file.Close()) - } -} - -func Test_parseCPUStat(t *testing.T) { - var testcases = []struct { - valid bool - line string - want cpuStat - }{ - { - valid: true, - line: "cpu 3097668 1593 1419618 132242258 42535 0 384686 0 0 0", - want: cpuStat{ - user: 30976.68, nice: 15.93, system: 14196.18, idle: 1322422.58, iowait: 425.35, - irq: 0, softirq: 3846.86, steal: 0, guest: 0, guestnice: 0, - }, - }, - {valid: false, line: "invalid 3097668 1593 1419618 132242258 42535 0 384686"}, - {valid: false, line: "invalid invalid"}, - } - - // assume that sys_ticks is 100 - for _, tc := range testcases { - got, err := parseCPUStat(tc.line, 100) - if tc.valid { - assert.NoError(t, err) - assert.Equal(t, tc.want, got) - } else { - assert.Error(t, err) - } - } -} - -func Test_getProcUptime(t *testing.T) { - up, idle, err := getProcUptime("testdata/proc/uptime.golden") - assert.NoError(t, err) - assert.Equal(t, float64(187477.470), up) - assert.Equal(t, float64(1397296.120), idle) - - _, _, err = getProcUptime("testdata/proc/stat.golden") - assert.Error(t, err) -} diff --git a/internal/collector/linux_filesystem.go b/internal/collector/linux_filesystem.go deleted file mode 100644 index 35e7cf9d..00000000 --- a/internal/collector/linux_filesystem.go +++ /dev/null @@ -1,213 +0,0 @@ -// Package collector is a pgSCV collectors -package collector - -import ( - "context" - "errors" - "fmt" - "io" - "os" - "syscall" - "time" - - "github.com/cherts/pgscv/internal/filter" - "github.com/cherts/pgscv/internal/log" - "github.com/cherts/pgscv/internal/model" - "github.com/prometheus/client_golang/prometheus" -) - -var ( - errFilesystemTimedOut = errors.New("filesystem timed out") -) - -type filesystemCollector struct { - bytes typedDesc - bytesTotal typedDesc - files typedDesc - filesTotal typedDesc -} - -// NewFilesystemCollector returns a new Collector exposing filesystem stats. -func NewFilesystemCollector(constLabels labels, settings model.CollectorSettings) (Collector, error) { - - // Define default filters (if no already present) to avoid collecting metrics about exotic filesystems. - if _, ok := settings.Filters["fstype"]; !ok { - if settings.Filters == nil { - settings.Filters = filter.New() - } - - settings.Filters.Add("fstype", filter.Filter{Include: `^(ext3|ext4|xfs|btrfs)$`}) - err := settings.Filters.Compile() - if err != nil { - return nil, err - } - } - - return &filesystemCollector{ - bytes: newBuiltinTypedDesc( - descOpts{"node", "filesystem", "bytes", "Number of bytes of filesystem by usage.", 0}, - prometheus.GaugeValue, - []string{"device", "mountpoint", "fstype", "usage"}, constLabels, - settings.Filters, - ), - bytesTotal: newBuiltinTypedDesc( - descOpts{"node", "filesystem", "bytes_total", "Total number of bytes of filesystem capacity.", 0}, - prometheus.GaugeValue, - []string{"device", "mountpoint", "fstype"}, constLabels, - settings.Filters, - ), - files: newBuiltinTypedDesc( - descOpts{"node", "filesystem", "files", "Number of files (inodes) of filesystem by usage.", 0}, - prometheus.GaugeValue, - []string{"device", "mountpoint", "fstype", "usage"}, constLabels, - settings.Filters, - ), - filesTotal: newBuiltinTypedDesc( - descOpts{"node", "filesystem", "files_total", "Total number of files (inodes) of filesystem capacity.", 0}, - prometheus.GaugeValue, - []string{"device", "mountpoint", "fstype"}, constLabels, - settings.Filters, - ), - }, nil -} - -// Update method collects filesystem usage statistics. -func (c *filesystemCollector) Update(_ context.Context, _ Config, ch chan<- prometheus.Metric) error { - stats, err := getFilesystemStats() - if err != nil { - return fmt.Errorf("get filesystem stats failed: %s", err) - } - - for _, s := range stats { - // Truncate device paths to device names, e.g /dev/sda -> sda - device := truncateDeviceName(s.mount.device) - - // bytes; free = avail + reserved; total = used + free - ch <- c.bytesTotal.newConstMetric(s.size, device, s.mount.mountpoint, s.mount.fstype) - ch <- c.bytes.newConstMetric(s.avail, device, s.mount.mountpoint, s.mount.fstype, "avail") - ch <- c.bytes.newConstMetric(s.free-s.avail, device, s.mount.mountpoint, s.mount.fstype, "reserved") - ch <- c.bytes.newConstMetric(s.size-s.free, device, s.mount.mountpoint, s.mount.fstype, "used") - // files (inodes) - ch <- c.filesTotal.newConstMetric(s.files, device, s.mount.mountpoint, s.mount.fstype) - ch <- c.files.newConstMetric(s.filesfree, device, s.mount.mountpoint, s.mount.fstype, "free") - ch <- c.files.newConstMetric(s.files-s.filesfree, device, s.mount.mountpoint, s.mount.fstype, "used") - } - - return nil -} - -// filesystemStat describes various stats related to filesystem usage. -type filesystemStat struct { - mount mount - size float64 - free float64 - avail float64 - files float64 - filesfree float64 - err error // error occurred during polling stats -} - -// getFilesystemStats opens stats file and execute stats parser. -func getFilesystemStats() ([]filesystemStat, error) { - file, err := os.Open("/proc/mounts") - if err != nil { - return nil, err - } - defer func() { _ = file.Close() }() - - return parseFilesystemStats(file) -} - -// parseFilesystemStats parses stats file and return stats. -func parseFilesystemStats(r io.Reader) ([]filesystemStat, error) { - mounts, err := parseProcMounts(r) - if err != nil { - return nil, err - } - - var stats []filesystemStat - for _, m := range mounts { - stat, err := readMountpointStat(m.mountpoint) - if err != nil { - log.Warnf("read %s stats failed: %s", m.mountpoint, err) - continue - } - - stats = append(stats, filesystemStat{ - mount: m, - size: stat.size, - free: stat.free, - avail: stat.avail, - files: stat.files, - filesfree: stat.filesfree, - }) - } - - return stats, nil -} - -// readMountpointStat requests stats from kernel and return filesystemStat if successful. -func readMountpointStat(mountpoint string) (filesystemStat, error) { - // Reading filesystem statistics might stuck, especially this is true for network filesystems. - // In such case reading stats done by child goroutine with timeout and allow it to hang. When - // timeout exceeds outside of child, return an error and left behind the spawned goroutine (it - // is impossible to forcibly interrupt stuck syscall). Hope when syscall finished at all, stat - // is discarded and goroutine finishes normally. - - timeout := 3 * time.Second // three seconds is sufficient to consider filesystem unresponsive. - statCh := make(chan *syscall.Statfs_t) - errCh := make(chan error) - - // Run goroutine with reading stats. Check kind of returned error. If error related to timeout, - // print warning and return. Other kinds of error should be reported to parent. - go func() { - s, err := readMountpointStatWithTimeout(mountpoint, timeout) - if err != nil { - if err == errFilesystemTimedOut { - log.Warnf("%s: %s, skip", mountpoint, err) - return - } - errCh <- err - } - - // Syscall successful - send stat to the channel. - statCh <- s - }() - - // Waiting for results of spawned goroutine or time out. - for { - select { - case s := <-statCh: - return filesystemStat{ - size: float64(s.Blocks) * float64(s.Bsize), - free: float64(s.Bfree) * float64(s.Bsize), - avail: float64(s.Bavail) * float64(s.Bsize), - files: float64(s.Files), - filesfree: float64(s.Ffree), - }, nil - case err := <-errCh: - return filesystemStat{err: err}, err - case <-time.After(timeout): - // Timeout expired, filesystem considered stuck, return. - return filesystemStat{err: errFilesystemTimedOut}, errFilesystemTimedOut - } - } -} - -// readMountpointStatWithTimeout read filesystem stats, discard data if reading exceeds timeout. -func readMountpointStatWithTimeout(mountpoint string, timeout time.Duration) (*syscall.Statfs_t, error) { - var buf syscall.Statfs_t - start := time.Now() - - err := syscall.Statfs(mountpoint, &buf) - if err != nil { - return nil, err - } - - if time.Since(start) > timeout { - log.Warnf("%s stats stale: %s", mountpoint, time.Since(start).String()) - return nil, errFilesystemTimedOut - } - - return &buf, nil -} diff --git a/internal/collector/linux_filesystem_test.go b/internal/collector/linux_filesystem_test.go deleted file mode 100644 index 4e3b7820..00000000 --- a/internal/collector/linux_filesystem_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package collector - -import ( - "github.com/cherts/pgscv/internal/filter" - "github.com/cherts/pgscv/internal/model" - "github.com/stretchr/testify/assert" - "os" - "path/filepath" - "testing" - "time" -) - -func TestFilesystemCollector_Update(t *testing.T) { - var input = pipelineInput{ - required: []string{ - "node_filesystem_bytes", - "node_filesystem_bytes_total", - "node_filesystem_files", - "node_filesystem_files_total", - }, - collector: NewFilesystemCollector, - collectorSettings: model.CollectorSettings{Filters: filter.New()}, - } - - pipeline(t, input) -} - -func Test_getFilesystemStats(t *testing.T) { - got, err := getFilesystemStats() - assert.NoError(t, err) - assert.NotNil(t, got) - assert.Greater(t, len(got), 0) -} - -func Test_parseFilesystemStats(t *testing.T) { - file, err := os.Open(filepath.Clean("testdata/proc/mounts.golden")) - assert.NoError(t, err) - - stats, err := parseFilesystemStats(file) - assert.NoError(t, err) - assert.Greater(t, len(stats), 1) - assert.Greater(t, stats[0].size, float64(0)) - assert.Greater(t, stats[0].free, float64(0)) - assert.Greater(t, stats[0].avail, float64(0)) - assert.Greater(t, stats[0].files, float64(0)) - assert.Greater(t, stats[0].filesfree, float64(0)) - - _ = file.Close() - - // test with wrong format file - file, err = os.Open(filepath.Clean("testdata/proc/netdev.golden")) - assert.NoError(t, err) - - stats, err = parseFilesystemStats(file) - assert.Error(t, err) - assert.Nil(t, stats) - _ = file.Close() -} - -func Test_readMountpointStat(t *testing.T) { - stat, err := readMountpointStat("/") - assert.NoError(t, err) - assert.Greater(t, stat.size, float64(0)) - assert.Greater(t, stat.free, float64(0)) - assert.Greater(t, stat.avail, float64(0)) - assert.Greater(t, stat.files, float64(0)) - assert.Greater(t, stat.filesfree, float64(0)) - - // unknown filesystem - stat, err = readMountpointStat("/invalid") - assert.Error(t, err) -} - -func Test_readMountpointStatWithTimeout(t *testing.T) { - stat, err := readMountpointStatWithTimeout("/", time.Second) - assert.NoError(t, err) - assert.Greater(t, stat.Blocks, uint64(0)) - - // unknown filesystem - _, err = readMountpointStatWithTimeout("/invalid", time.Second) - assert.Error(t, err) -} diff --git a/internal/collector/linux_load_average_test.go b/internal/collector/linux_load_average_test.go deleted file mode 100644 index bc085100..00000000 --- a/internal/collector/linux_load_average_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package collector - -import ( - "github.com/stretchr/testify/assert" - "os" - "testing" -) - -func TestLoadAverageCollector_Update(t *testing.T) { - var input = pipelineInput{ - required: []string{ - "node_load1", - "node_load5", - "node_load15", - }, - collector: NewLoadAverageCollector, - } - - pipeline(t, input) -} - -func Test_getLoadAverageStats(t *testing.T) { - loads, err := getLoadAverageStats() - assert.NoError(t, err) - assert.Len(t, loads, 3) -} - -func Test_parseLoadAverageStats(t *testing.T) { - data, err := os.ReadFile("./testdata/proc/loadavg.golden") - assert.NoError(t, err) - - loads, err := parseLoadAverageStats(string(data)) - assert.NoError(t, err) - assert.Equal(t, 1.15, loads[0]) - assert.Equal(t, 1.36, loads[1]) - assert.Equal(t, 1.24, loads[2]) - - _, err = parseLoadAverageStats("invalid data") - assert.Error(t, err) - - _, err = parseLoadAverageStats("1 qq 2 1/123 12312") - assert.Error(t, err) -} diff --git a/internal/collector/linux_memory_test.go b/internal/collector/linux_memory_test.go deleted file mode 100644 index 4def0d41..00000000 --- a/internal/collector/linux_memory_test.go +++ /dev/null @@ -1,249 +0,0 @@ -package collector - -import ( - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestMeminfoCollector_Update(t *testing.T) { - meminfo, err := os.ReadFile("/proc/meminfo") - assert.NoError(t, err) - fmt.Println(string(meminfo)) - - vmstat, err := os.ReadFile("/proc/vmstat") - assert.NoError(t, err) - fmt.Println(string(vmstat)) - - var input = pipelineInput{ - required: []string{ - "node_memory_MemTotal", "node_memory_MemFree", "node_memory_MemAvailable", "node_memory_MemUsed", - "node_memory_Buffers", "node_memory_Cached", "node_memory_SwapCached", - "node_memory_Active", "node_memory_Inactive", "node_memory_Active_anon", - "node_memory_Inactive_anon", "node_memory_Active_file", "node_memory_Inactive_file", - "node_memory_SwapTotal", "node_memory_SwapFree", "node_memory_SwapUsed", - "node_memory_Dirty", "node_memory_Writeback", "node_memory_AnonPages", "node_memory_Mapped", - "node_memory_Shmem", "node_memory_PageTables", "node_memory_HugePages_Total", - "node_memory_HugePages_Free", "node_memory_HugePages_Rsvd", "node_memory_HugePages_Surp", - "node_memory_Hugepagesize", - // vmstat - "node_vmstat_nr_anon_pages", "node_vmstat_nr_mapped", "node_vmstat_nr_dirty", "node_vmstat_nr_writeback", - "node_vmstat_pgpgin", "node_vmstat_pgpgout", "node_vmstat_pswpin", "node_vmstat_pswpout", - }, - optional: []string{ - // meminfo - "node_memory_Bounce", "node_memory_FilePmdMapped", "node_memory_CmaFree", "node_memory_ShmemHugePages", - "node_memory_KReclaimable", "node_memory_CommitLimit", "node_memory_Slab", "node_memory_AnonHugePages", - "node_memory_FileHugePages", "node_memory_DirectMap4k", "node_memory_VmallocTotal", "node_memory_SReclaimable", - "node_memory_VmallocUsed", "node_memory_DirectMap1G", "node_memory_Committed_AS", "node_memory_Unevictable", - "node_memory_WritebackTmp", "node_memory_NFS_Unstable", "node_memory_DirectMap2M", "node_memory_Hugetlb", - "node_memory_CmaTotal", "node_memory_Mlocked", "node_memory_ShmemPmdMapped", "node_memory_SUnreclaim", - "node_memory_KernelStack", "node_memory_VmallocChunk", "node_memory_Percpu", "node_memory_HardwareCorrupted", - "node_memory_CmaFree", "node_memory_CmaTotal", "node_memory_Zswap", "node_memory_Zswapped", - "node_memory_SecPageTables", "node_memory_Unaccepted", - // vmstat - "node_vmstat_nr_free_pages", "node_vmstat_nr_zone_inactive_anon", "node_vmstat_nr_zone_active_anon", - "node_vmstat_nr_zone_inactive_file", "node_vmstat_nr_zone_active_file", "node_vmstat_nr_zone_unevictable", - "node_vmstat_nr_zone_write_pending", "node_vmstat_nr_mlock", "node_vmstat_nr_page_table_pages", - "node_vmstat_nr_kernel_stack", "node_vmstat_nr_bounce", "node_vmstat_nr_zspages", "node_vmstat_nr_free_cma", - "node_vmstat_numa_hit", "node_vmstat_numa_miss", "node_vmstat_numa_foreign", "node_vmstat_numa_interleave", - "node_vmstat_numa_local", "node_vmstat_numa_other", "node_vmstat_nr_inactive_anon", "node_vmstat_nr_active_anon", - "node_vmstat_nr_inactive_file", "node_vmstat_nr_active_file", "node_vmstat_nr_unevictable", - "node_vmstat_nr_slab_reclaimable", "node_vmstat_nr_slab_unreclaimable", "node_vmstat_nr_isolated_anon", - "node_vmstat_nr_isolated_file", "node_vmstat_workingset_nodes", "node_vmstat_workingset_refault", - "node_vmstat_workingset_activate", "node_vmstat_workingset_restore", "node_vmstat_workingset_nodereclaim", - "node_vmstat_nr_file_pages", "node_vmstat_nr_writeback_temp", "node_vmstat_nr_shmem", - "node_vmstat_nr_shmem_hugepages", "node_vmstat_nr_shmem_pmdmapped", "node_vmstat_nr_file_hugepages", - "node_vmstat_nr_file_pmdmapped", "node_vmstat_nr_anon_transparent_hugepages", "node_vmstat_nr_unstable", - "node_vmstat_nr_vmscan_write", "node_vmstat_nr_vmscan_immediate_reclaim", "node_vmstat_nr_dirtied", - "node_vmstat_nr_written", "node_vmstat_nr_kernel_misc_reclaimable", "node_vmstat_nr_dirty_threshold", - "node_vmstat_nr_dirty_background_threshold", "node_vmstat_pgalloc_dma", "node_vmstat_pgalloc_dma32", - "node_vmstat_pgalloc_normal", "node_vmstat_pgalloc_movable", "node_vmstat_allocstall_dma", - "node_vmstat_allocstall_dma32", "node_vmstat_allocstall_normal", "node_vmstat_allocstall_movable", - "node_vmstat_pgskip_dma", "node_vmstat_pgskip_dma32", "node_vmstat_pgskip_normal", - "node_vmstat_pgskip_movable", "node_vmstat_pgfree", "node_vmstat_pgactivate", "node_vmstat_pgdeactivate", - "node_vmstat_pglazyfree", "node_vmstat_pgfault", "node_vmstat_pgmajfault", "node_vmstat_pglazyfreed", - "node_vmstat_pgrefill", "node_vmstat_pgsteal_kswapd", "node_vmstat_pgsteal_direct", - "node_vmstat_pgscan_kswapd", "node_vmstat_pgscan_direct", "node_vmstat_pgscan_direct_throttle", - "node_vmstat_zone_reclaim_failed", "node_vmstat_pginodesteal", "node_vmstat_slabs_scanned", - "node_vmstat_kswapd_inodesteal", "node_vmstat_kswapd_low_wmark_hit_quickly", - "node_vmstat_kswapd_high_wmark_hit_quickly", "node_vmstat_pageoutrun", "node_vmstat_pgrotated", - "node_vmstat_drop_pagecache", "node_vmstat_drop_slab", "node_vmstat_oom_kill", "node_vmstat_numa_pte_updates", - "node_vmstat_numa_huge_pte_updates", "node_vmstat_numa_hint_faults", "node_vmstat_numa_hint_faults_local", - "node_vmstat_numa_pages_migrated", "node_vmstat_pgmigrate_success", "node_vmstat_pgmigrate_fail", - "node_vmstat_compact_migrate_scanned", "node_vmstat_compact_free_scanned", "node_vmstat_compact_isolated", - "node_vmstat_compact_stall", "node_vmstat_compact_fail", "node_vmstat_compact_success", - "node_vmstat_compact_daemon_wake", "node_vmstat_compact_daemon_migrate_scanned", - "node_vmstat_compact_daemon_free_scanned", "node_vmstat_htlb_buddy_alloc_success", - "node_vmstat_htlb_buddy_alloc_fail", "node_vmstat_unevictable_pgs_culled", - "node_vmstat_unevictable_pgs_scanned", "node_vmstat_unevictable_pgs_rescued", - "node_vmstat_unevictable_pgs_mlocked", "node_vmstat_unevictable_pgs_munlocked", - "node_vmstat_unevictable_pgs_cleared", "node_vmstat_unevictable_pgs_stranded", - "node_vmstat_thp_fault_alloc", "node_vmstat_thp_fault_fallback", "node_vmstat_thp_collapse_alloc", - "node_vmstat_thp_collapse_alloc_failed", "node_vmstat_thp_file_alloc", "node_vmstat_thp_file_mapped", - "node_vmstat_thp_split_page", "node_vmstat_thp_split_page_failed", "node_vmstat_thp_deferred_split_page", - "node_vmstat_thp_split_pmd", "node_vmstat_thp_split_pud", "node_vmstat_thp_zero_page_alloc", - "node_vmstat_thp_zero_page_alloc_failed", "node_vmstat_thp_swpout", "node_vmstat_thp_swpout_fallback", - "node_vmstat_thp_migration_fail", "node_vmstat_workingset_restore_anon", "node_vmstat_workingset_activate_file", - "node_vmstat_workingset_refault_file", "node_vmstat_thp_migration_split", "node_vmstat_workingset_refault_anon", - "node_vmstat_pgreuse", "node_vmstat_thp_migration_success", "node_vmstat_workingset_activate_anon", - "node_vmstat_workingset_restore_file", "node_vmstat_balloon_inflate", "node_vmstat_balloon_deflate", - "node_vmstat_balloon_migrate", "node_vmstat_swap_ra", "node_vmstat_swap_ra_hit", "node_vmstat_nr_foll_pin_acquired", - "node_vmstat_pgsteal_anon", "node_vmstat_pgsteal_file", "node_vmstat_pgscan_file", "node_vmstat_pgscan_anon", - "node_vmstat_thp_file_fallback_charge", "node_vmstat_nr_foll_pin_released", "node_vmstat_thp_file_fallback", - "node_vmstat_thp_fault_fallback_charge", "node_vmstat_nr_swapcached", "node_vmstat_direct_map_level2_splits", - "node_vmstat_direct_map_level3_splits", "node_vmstat_workingset_refault", "node_vmstat_workingset_activate", - "node_vmstat_workingset_restore", "node_vmstat_pgdemote_kswapd", "node_vmstat_pgdemote_direct", - "node_vmstat_pgsteal_khugepaged", "node_vmstat_pgskip_device", "node_vmstat_pgdemote_khugepaged", - "node_vmstat_thp_scan_exceed_none_pte", "node_vmstat_nr_throttled_written", "node_vmstat_thp_scan_exceed_share_pte", - "node_vmstat_pgpromote_candidate", "node_vmstat_pgscan_khugepaged", "node_vmstat_zswpin", - "node_vmstat_thp_scan_exceed_swap_pte", "node_vmstat_allocstall_device", "node_vmstat_pgpromote_success", - "node_vmstat_pgalloc_device", "node_vmstat_nr_sec_page_table_pages", "node_vmstat_cow_ksm", - "node_vmstat_ksm_swpin_copy", "node_vmstat_zswpout", "node_vmstat_nr_unaccepted", "node_vmstat_zswpwb", - }, - collector: NewMeminfoCollector, - } - - pipeline(t, input) -} - -func Test_getMeminfoStats(t *testing.T) { - s, err := getMeminfoStats() - assert.NoError(t, err) - assert.Greater(t, len(s), 0) -} - -func Test_parseMeminfoStats(t *testing.T) { - file, err := os.Open(filepath.Clean("testdata/proc/meminfo.golden")) - assert.NoError(t, err) - defer func() { _ = file.Close() }() - - stats, err := parseMeminfoStats(file) - assert.NoError(t, err) - - want := map[string]float64{ - "MemTotal": 32839484 * 1024, - "MemFree": 21570088 * 1024, - "MemAvailable": 26190600 * 1024, - "Buffers": 604064 * 1024, - "Cached": 4361844 * 1024, - "SwapCached": 0 * 1024, - "Active": 7785324 * 1024, - "Inactive": 2591484 * 1024, - "Active(anon)": 5448748 * 1024, - "Inactive(anon)": 344784 * 1024, - "Active(file)": 2336576 * 1024, - "Inactive(file)": 2246700 * 1024, - "Unevictable": 0 * 1024, - "Mlocked": 0 * 1024, - "SwapTotal": 16777212 * 1024, - "SwapFree": 16777212 * 1024, - "Dirty": 36404 * 1024, - "Writeback": 0 * 1024, - "AnonPages": 5410948 * 1024, - "Mapped": 1197820 * 1024, - "Shmem": 386884 * 1024, - "KReclaimable": 502080 * 1024, - "Slab": 692516 * 1024, - "SReclaimable": 502080 * 1024, - "SUnreclaim": 190436 * 1024, - "KernelStack": 16848 * 1024, - "PageTables": 54472 * 1024, - "NFS_Unstable": 0 * 1024, - "Bounce": 0 * 1024, - "WritebackTmp": 0 * 1024, - "CommitLimit": 33196952 * 1024, - "Committed_AS": 12808144 * 1024, - "VmallocTotal": 34359738367 * 1024, - "VmallocUsed": 34976 * 1024, - "VmallocChunk": 0 * 1024, - "Percpu": 6528 * 1024, - "HardwareCorrupted": 0 * 1024, - "AnonHugePages": 0 * 1024, - "ShmemHugePages": 0 * 1024, - "ShmemPmdMapped": 0 * 1024, - "FileHugePages": 0 * 1024, - "FilePmdMapped": 0 * 1024, - "CmaTotal": 0 * 1024, - "CmaFree": 0 * 1024, - "HugePages_Total": 0 * 2048 * 1024, - "HugePages_Free": 0 * 2048 * 1024, - "HugePages_Rsvd": 0 * 2048 * 1024, - "HugePages_Surp": 0 * 2048 * 1024, - "Hugepagesize": 2048 * 1024, - "Hugetlb": 0 * 1024, - "DirectMap4k": 482128 * 1024, - "DirectMap2M": 13101056 * 1024, - "DirectMap1G": 19922944 * 1024, - } - - assert.Equal(t, want, stats) - - // test with wrong format file - file, err = os.Open(filepath.Clean("testdata/proc/netdev.golden")) - assert.NoError(t, err) - defer func() { _ = file.Close() }() - - stats, err = parseMeminfoStats(file) - assert.Error(t, err) - assert.Nil(t, stats) -} - -func Test_getVmstatStats(t *testing.T) { - s, err := getVmstatStats() - assert.NoError(t, err) - assert.Greater(t, len(s), 0) -} - -func Test_parseVmstatStats(t *testing.T) { - file, err := os.Open(filepath.Clean("testdata/proc/vmstat.golden")) - assert.NoError(t, err) - - stats, err := parseVmstatStats(file) - assert.NoError(t, err) - - wantStats := map[string]float64{ - "oom_kill": 10, - "nr_zone_active_file": 1933629, - "nr_unevictable": 24, - "nr_writeback": 0, - "pgactivate": 57995375, - } - - for k, want := range wantStats { - if got, ok := stats[k]; ok { - assert.Equal(t, want, got) - } else { - assert.Fail(t, "not found") - } - } - - assert.NoError(t, file.Close()) - - // test with invalid values - file, err = os.Open(filepath.Clean("testdata/proc/vmstat.invalid.1")) - assert.NoError(t, err) - stats, err = parseVmstatStats(file) - assert.NoError(t, err) - assert.Equal(t, 3, len(stats)) - assert.NoError(t, file.Close()) - - // test with wrong number of fields - file, err = os.Open(filepath.Clean("testdata/proc/vmstat.invalid.2")) - assert.NoError(t, err) - _, err = parseVmstatStats(file) - assert.Error(t, err) - assert.NoError(t, file.Close()) - - // test with wrong format file - file, err = os.Open(filepath.Clean("testdata/proc/netdev.golden")) - assert.NoError(t, err) - - stats, err = parseVmstatStats(file) - assert.Error(t, err) - assert.Nil(t, stats) - assert.NoError(t, file.Close()) -} diff --git a/internal/collector/linux_netdev.go b/internal/collector/linux_netdev.go deleted file mode 100644 index f6aef7de..00000000 --- a/internal/collector/linux_netdev.go +++ /dev/null @@ -1,148 +0,0 @@ -// Package collector is a pgSCV collectors -package collector - -import ( - "bufio" - "context" - "fmt" - "io" - "os" - "strconv" - "strings" - - "github.com/cherts/pgscv/internal/filter" - "github.com/cherts/pgscv/internal/log" - "github.com/cherts/pgscv/internal/model" - "github.com/prometheus/client_golang/prometheus" -) - -type netdevCollector struct { - bytes typedDesc - packets typedDesc - events typedDesc -} - -// NewNetdevCollector returns a new Collector exposing network interfaces stats. -func NewNetdevCollector(constLabels labels, settings model.CollectorSettings) (Collector, error) { - - // Define default filters (if no already present) to avoid collecting metrics about virtual interfaces. - if _, ok := settings.Filters["device"]; !ok { - if settings.Filters == nil { - settings.Filters = filter.New() - } - - settings.Filters.Add("device", filter.Filter{Exclude: `docker|virbr`}) - err := settings.Filters.Compile() - if err != nil { - return nil, err - } - } - - return &netdevCollector{ - bytes: newBuiltinTypedDesc( - descOpts{"node", "network", "bytes_total", "Total number of bytes processed by network device, by each direction.", 0}, - prometheus.CounterValue, - []string{"device", "type"}, constLabels, - settings.Filters, - ), - packets: newBuiltinTypedDesc( - descOpts{"node", "network", "packets_total", "Total number of packets processed by network device, by each direction.", 0}, - prometheus.CounterValue, - []string{"device", "type"}, constLabels, - settings.Filters, - ), - events: newBuiltinTypedDesc( - descOpts{"node", "network", "events_total", "Total number of events occurred on network device, by each type and direction.", 0}, - prometheus.CounterValue, - []string{"device", "type", "event"}, constLabels, - settings.Filters, - ), - }, nil -} - -// Update method collects network interfaces statistics -func (c *netdevCollector) Update(_ context.Context, _ Config, ch chan<- prometheus.Metric) error { - stats, err := getNetdevStats() - if err != nil { - return fmt.Errorf("get /proc/net/dev stats failed: %s", err) - } - - for device, stat := range stats { - if len(stat) < 16 { - log.Warnf("too few stats columns (%d), skip", len(stat)) - continue - } - - // recv - ch <- c.bytes.newConstMetric(stat[0], device, "recv") - ch <- c.packets.newConstMetric(stat[1], device, "recv") - ch <- c.events.newConstMetric(stat[2], device, "recv", "errs") - ch <- c.events.newConstMetric(stat[3], device, "recv", "drop") - ch <- c.events.newConstMetric(stat[4], device, "recv", "fifo") - ch <- c.events.newConstMetric(stat[5], device, "recv", "frame") - ch <- c.events.newConstMetric(stat[6], device, "recv", "compressed") - ch <- c.events.newConstMetric(stat[7], device, "recv", "multicast") - - // sent - ch <- c.bytes.newConstMetric(stat[8], device, "sent") - ch <- c.packets.newConstMetric(stat[9], device, "sent") - ch <- c.events.newConstMetric(stat[10], device, "sent", "errs") - ch <- c.events.newConstMetric(stat[11], device, "sent", "drop") - ch <- c.events.newConstMetric(stat[12], device, "sent", "fifo") - ch <- c.events.newConstMetric(stat[13], device, "sent", "colls") - ch <- c.events.newConstMetric(stat[14], device, "sent", "carrier") - ch <- c.events.newConstMetric(stat[15], device, "sent", "compressed") - } - - return nil -} - -// getNetdevStats is the intermediate function which opens stats file and run stats parser for extracting stats. -func getNetdevStats() (map[string][]float64, error) { - file, err := os.Open("/proc/net/dev") - if err != nil { - return nil, err - } - defer func() { _ = file.Close() }() - - return parseNetdevStats(file) -} - -// parseNetdevStats accepts file descriptor, reads file content and produces stats. -func parseNetdevStats(r io.Reader) (map[string][]float64, error) { - log.Debug("parse network devices stats") - - scanner := bufio.NewScanner(r) - - // Stats file /proc/net/dev has header consisting of two lines. Read the header and check content to make sure this is proper file. - for range 2 { - scanner.Scan() - parts := strings.Split(scanner.Text(), "|") - if len(parts) != 3 { - return nil, fmt.Errorf("invalid input, '%s': wrong number of values", scanner.Text()) - } - } - - var stats = map[string][]float64{} - - for scanner.Scan() { - values := strings.Fields(scanner.Text()) - - device := strings.TrimRight(values[0], ":") - - // Create float64 slice for values, parse line except first three values (major/minor/device) - stat := make([]float64, len(values)-1) - for i := range stat { - value, err := strconv.ParseFloat(values[i+1], 64) - if err != nil { - log.Errorf("invalid input, parse '%s' failed: %s, skip", values[i+1], err.Error()) - continue - } - stat[i] = value - } - - stats[device] = stat - } - - return stats, scanner.Err() -} diff --git a/internal/collector/linux_netdev_test.go b/internal/collector/linux_netdev_test.go deleted file mode 100644 index 399819d6..00000000 --- a/internal/collector/linux_netdev_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package collector - -import ( - "github.com/cherts/pgscv/internal/filter" - "github.com/cherts/pgscv/internal/model" - "github.com/stretchr/testify/assert" - "os" - "path/filepath" - "testing" -) - -func TestNetdevCollector_Update(t *testing.T) { - var input = pipelineInput{ - required: []string{ - "node_network_bytes_total", - "node_network_packets_total", - "node_network_events_total", - }, - collector: NewNetdevCollector, - collectorSettings: model.CollectorSettings{Filters: filter.New()}, - } - - pipeline(t, input) -} - -func Test_parseNetdevStats(t *testing.T) { - file, err := os.Open(filepath.Clean("testdata/proc/netdev.golden")) - assert.NoError(t, err) - defer func() { _ = file.Close() }() - - stats, err := parseNetdevStats(file) - assert.NoError(t, err) - - want := map[string][]float64{ - "enp2s0": {34899781, 60249, 10, 20, 30, 40, 50, 447, 8935189, 63211, 60, 70, 80, 90, 100, 110}, - "lo": {31433180, 57694, 15, 25, 48, 75, 71, 18, 31433180, 57694, 12, 48, 82, 38, 66, 17}, - "wlxc8be19e6279d": {68384665, 50991, 0, 85, 0, 17, 0, 44, 4138903, 29619, 1, 0, 74, 0, 4, 0}, - } - - assert.Equal(t, want, stats) -} diff --git a/internal/collector/linux_sysconfig.go b/internal/collector/linux_sysconfig.go index f599e226..7414e93e 100644 --- a/internal/collector/linux_sysconfig.go +++ b/internal/collector/linux_sysconfig.go @@ -11,6 +11,7 @@ import ( "path" "path/filepath" "regexp" + "runtime" "strconv" "strings" @@ -27,7 +28,6 @@ type systemCollector struct { numanodes typedDesc ctxt typedDesc forks typedDesc - btime typedDesc } // NewSysconfigCollector returns a new Collector exposing system-wide stats. @@ -83,58 +83,58 @@ func NewSysconfigCollector(constLabels labels, settings model.CollectorSettings) nil, constLabels, settings.Filters, ), - btime: newBuiltinTypedDesc( - descOpts{"node", "", "boot_time_seconds", "Node boot time, in unixtime.", 0}, - prometheus.GaugeValue, - nil, constLabels, - settings.Filters, - ), }, nil } // Update method collects filesystem usage statistics. func (c *systemCollector) Update(_ context.Context, _ Config, ch chan<- prometheus.Metric) error { - sysctls := readSysctls(c.sysctlList) + if runtime.GOOS == "linux" { + // Read sysctl settings. + sysctls := readSysctls(c.sysctlList) - for name, value := range sysctls { - ch <- c.sysctl.newConstMetric(value, name) - } + for name, value := range sysctls { + ch <- c.sysctl.newConstMetric(value, name) + } - // Count CPU cores by state. - cpuonline, cpuoffline, err := countCPUCores("/sys/devices/system/cpu/cpu*") - if err != nil { - log.Warnf("cpu count failed: %s; skip", err) - } else { - ch <- c.cpucores.newConstMetric(cpuonline, "online") - ch <- c.cpucores.newConstMetric(cpuoffline, "offline") - } + // Count CPU cores by state. + cpuonline, cpuoffline, err := countCPUCores("/sys/devices/system/cpu/cpu*") + if err != nil { + log.Warnf("cpu count failed: %s; skip", err) + } else { + ch <- c.cpucores.newConstMetric(cpuonline, "online") + ch <- c.cpucores.newConstMetric(cpuoffline, "offline") + } - // Count CPU scaling governors. - governors, err := countScalingGovernors("/sys/devices/system/cpu/cpu*") - if err != nil { - log.Warnf("count CPU scaling governors failed: %s; skip", err) - } else { - for governor, total := range governors { - ch <- c.governors.newConstMetric(total, governor) + // Count CPU scaling governors. + governors, err := countScalingGovernors("/sys/devices/system/cpu/cpu*") + if err != nil { + log.Warnf("count CPU scaling governors failed: %s; skip", err) + } else { + for governor, total := range governors { + ch <- c.governors.newConstMetric(total, governor) + } } - } - // Count NUMA nodes. - nodes, err := countNumaNodes("/sys/devices/system/node/node*") - if err != nil { - log.Warnf("count NUMA nodes failed: %s; skip", err) - } else { - ch <- c.numanodes.newConstMetric(nodes) - } + // Count NUMA nodes. + nodes, err := countNumaNodes("/sys/devices/system/node/node*") + if err != nil { + log.Warnf("count NUMA nodes failed: %s; skip", err) + } else { + ch <- c.numanodes.newConstMetric(nodes) + } - // Collect /proc/stat based metrics. - stat, err := getProcStat() - if err != nil { - log.Warnf("parse /proc/stat failed: %s; skip", err) + // Collect /proc/stat based metrics. + stat, err := getProcStat() + if err != nil { + log.Warnf("parse /proc/stat failed: %s; skip", err) + } else { + ch <- c.ctxt.newConstMetric(stat.ctxt) + ch <- c.forks.newConstMetric(stat.forks) + } } else { - ch <- c.ctxt.newConstMetric(stat.ctxt) - ch <- c.btime.newConstMetric(stat.btime) - ch <- c.forks.newConstMetric(stat.forks) + ch <- c.numanodes.newConstMetric(-1) + ch <- c.ctxt.newConstMetric(-1) + ch <- c.forks.newConstMetric(-1) } return nil diff --git a/internal/collector/linux_sysconfig_test.go b/internal/collector/linux_sysconfig_test.go index d7c2e9fc..d111fe26 100644 --- a/internal/collector/linux_sysconfig_test.go +++ b/internal/collector/linux_sysconfig_test.go @@ -1,10 +1,11 @@ package collector import ( - "github.com/stretchr/testify/assert" "os" "path/filepath" "testing" + + "github.com/stretchr/testify/assert" ) func TestSystemCollector_Update(t *testing.T) { @@ -15,7 +16,6 @@ func TestSystemCollector_Update(t *testing.T) { "node_system_numa_nodes_total", "node_context_switches_total", "node_forks_total", - "node_boot_time_seconds", }, optional: []string{ "node_system_scaling_governors_total", diff --git a/internal/collector/linux_sysinfo.go b/internal/collector/linux_sysinfo.go deleted file mode 100644 index a9d8f558..00000000 --- a/internal/collector/linux_sysinfo.go +++ /dev/null @@ -1,144 +0,0 @@ -// Package collector is a pgSCV collectors -package collector - -import ( - "bufio" - "context" - "fmt" - "io" - "os" - "path/filepath" - "strings" - - "github.com/cherts/pgscv/internal/log" - "github.com/cherts/pgscv/internal/model" - "github.com/prometheus/client_golang/prometheus" -) - -type sysinfoCollector struct { - platform typedDesc - os typedDesc -} - -// NewSysInfoCollector returns a new Collector exposing system info. -func NewSysInfoCollector(constLabels labels, settings model.CollectorSettings) (Collector, error) { - return &sysinfoCollector{ - platform: newBuiltinTypedDesc( - descOpts{"node", "platform", "info", "Labeled system platform information", 0}, - prometheus.GaugeValue, - []string{"vendor", "product_name"}, constLabels, - settings.Filters, - ), - os: newBuiltinTypedDesc( - descOpts{"node", "os", "info", "Labeled operating system information.", 0}, - prometheus.GaugeValue, - []string{"kernel", "type", "name", "version"}, constLabels, - settings.Filters, - ), - }, nil -} - -// Update implements Collector and exposes system info metrics. -func (c *sysinfoCollector) Update(_ context.Context, _ Config, ch chan<- prometheus.Metric) error { - info, err := getSysInfo() - if err != nil { - return err - } - - ch <- c.platform.newConstMetric(1, info.sysVendor, info.productName) - ch <- c.os.newConstMetric(1, info.kernel, info.osType, info.osName, info.osVersion) - - return nil -} - -// sysInfo contains various information about platform and operating system. -type sysInfo struct { - sysVendor string - productName string - kernel string - osType string - osName string - osVersion string -} - -// getSysInfo reads various information about platform and system. -func getSysInfo() (*sysInfo, error) { - vendor, err := os.ReadFile("/sys/class/dmi/id/sys_vendor") - if err != nil { - return nil, err - } - - name, err := os.ReadFile("/sys/class/dmi/id/product_name") - if err != nil { - return nil, err - } - - kernel, err := os.ReadFile("/proc/sys/kernel/osrelease") - if err != nil { - return nil, err - } - - osType, err := os.ReadFile("/proc/sys/kernel/ostype") - if err != nil { - return nil, err - } - - osName, osVersion, err := getOsRelease() - if err != nil { - return nil, err - } - - return &sysInfo{ - sysVendor: strings.TrimSpace(string(vendor)), - productName: strings.TrimSpace(string(name)), - kernel: strings.TrimSpace(string(kernel)), - osType: strings.TrimSpace(string(osType)), - osName: osName, - osVersion: osVersion, - }, nil -} - -// getOsRelease reads content of /etc/os-release and returns OS name and version. -func getOsRelease() (string, string, error) { - file, err := os.Open(filepath.Clean("/etc/os-release")) - if err != nil { - return "", "", err - } - defer func() { _ = file.Close() }() - - return parseOsRelease(file) -} - -// parseOsRelease scans buffer data for OS name and version. -func parseOsRelease(r io.Reader) (string, string, error) { - log.Debug("parse os-release info") - - scanner := bufio.NewScanner(r) - var name, version string - - for scanner.Scan() { - // Skip empty lines - if scanner.Text() == "" { - continue - } - - parts := strings.SplitN(scanner.Text(), "=", 2) - - if len(parts) != 2 { - return "", "", fmt.Errorf("invalid input, '%s': wrong number of values", scanner.Text()) - } - - key, value := parts[0], parts[1] - - switch key { - case "NAME": - name = strings.Trim(value, `"`) - case "VERSION": - version = strings.Trim(value, `"`) - default: - continue - } - } - - return name, version, scanner.Err() -} diff --git a/internal/collector/postgres_logs.go b/internal/collector/postgres_logs.go index 5e6934fb..cb59c2a8 100644 --- a/internal/collector/postgres_logs.go +++ b/internal/collector/postgres_logs.go @@ -5,6 +5,8 @@ import ( "context" "fmt" "io" + "os" + "path/filepath" "regexp" "strings" "sync" @@ -114,6 +116,7 @@ func (c *postgresLogsCollector) Update(_ context.Context, config Config, ch chan } if !config.loggingCollector { + log.Debugln("[postgres log collector]: logging collector in PostgreSQL is off, skip collecting metrics") return nil } @@ -265,6 +268,13 @@ func queryCurrentLogfile(config Config) (string, error) { logfile = datadir + "/" + logfile } + logfile = filepath.FromSlash(logfile) + + // You can also directly use os.PathSeparator for replacement if needed + if os.PathSeparator == '\\' { // If on Windows + logfile = strings.ReplaceAll(logfile, "/", string(os.PathSeparator)) + } + return logfile, nil } diff --git a/internal/collector/postgres_storage.go b/internal/collector/postgres_storage.go index 7d90827c..d3a179af 100644 --- a/internal/collector/postgres_storage.go +++ b/internal/collector/postgres_storage.go @@ -13,6 +13,7 @@ import ( "github.com/cherts/pgscv/internal/log" "github.com/cherts/pgscv/internal/model" "github.com/prometheus/client_golang/prometheus" + "github.com/shirou/gopsutil/v4/disk" ) const ( @@ -677,13 +678,14 @@ func findMountpoint(mounts []mount, path string) (string, string, error) { return findMountpoint(mounts, path) } -// getMountpoints opens /proc/mounts file and run parser. +// getMountpoints get list of partitions and run parser. func getMountpoints() ([]mount, error) { - file, err := os.Open("/proc/mounts") + log.Debug("get disk partitions") + + diskStat, err := disk.Partitions(false) if err != nil { return nil, err } - defer func() { _ = file.Close() }() - return parseProcMounts(file) + return parseMounts(diskStat) } diff --git a/internal/collector/testdata/etc/os-release.golden b/internal/collector/testdata/etc/os-release.golden deleted file mode 100644 index 2c364df5..00000000 --- a/internal/collector/testdata/etc/os-release.golden +++ /dev/null @@ -1,13 +0,0 @@ -NAME="Ubuntu" -VERSION="20.04.2 LTS (Focal Fossa)" -ID=ubuntu -ID_LIKE=debian - -PRETTY_NAME="Ubuntu 20.04.2 LTS" -VERSION_ID="20.04" -HOME_URL="https://www.ubuntu.com/" -SUPPORT_URL="https://help.ubuntu.com/" -BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" -PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" -VERSION_CODENAME=focal -UBUNTU_CODENAME=focal diff --git a/internal/collector/testdata/proc/loadavg.golden b/internal/collector/testdata/proc/loadavg.golden deleted file mode 100644 index 52a18f95..00000000 --- a/internal/collector/testdata/proc/loadavg.golden +++ /dev/null @@ -1 +0,0 @@ -1.15 1.36 1.24 1/2076 2253587 diff --git a/internal/collector/testdata/proc/mounts.golden b/internal/collector/testdata/proc/mounts.golden deleted file mode 100644 index 7776c45b..00000000 --- a/internal/collector/testdata/proc/mounts.golden +++ /dev/null @@ -1,4 +0,0 @@ -/dev/mapper/ssd-root / ext4 rw,relatime,discard,errors=remount-ro 0 0 -/dev/sda1 /boot ext3 rw,relatime 0 0 -/dev/mapper/ssd-data /data ext4 rw,relatime,discard 0 0 -/dev/sdc1 /archive xfs rw,relatime 0 0 \ No newline at end of file