Repository: pdf/zfs_exporter Branch: master Commit: 0aacc90c6759 Files: 29 Total size: 111.8 KB Directory structure: gitextract__6skz3q_/ ├── .github/ │ ├── CONTRIBUTING.md │ └── workflows/ │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .promu.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── Makefile.common ├── README.md ├── VERSION ├── collector/ │ ├── cache.go │ ├── collector.go │ ├── collector_test.go │ ├── dataset.go │ ├── dataset_test.go │ ├── pool.go │ ├── pool_test.go │ ├── transform.go │ ├── zfs.go │ └── zfs_test.go ├── go.mod ├── go.sum ├── zfs/ │ ├── dataset.go │ ├── mock_zfs/ │ │ └── mock_zfs.go │ ├── pool.go │ └── zfs.go └── zfs_exporter.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing When contributing to this repository, please open an issue with a description of the problem you wish to solve, prior to sending a pull request. ## Contributing Code Please ensure that all code is formatted prior to committing. ### Commit messages Commits to this repository should have messages that conform to the [AngularJS Git Commit Guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines). ================================================ FILE: .github/workflows/release.yml ================================================ # This is a basic workflow to help you get started with Actions name: Release # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the master branch on: push: branches: ["master"] env: PARALLELISM: 3 # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "release" release: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: - name: Go Report Card uses: creekorful/goreportcard-action@v1.0 # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: Checkout id: checkout uses: actions/checkout@v2 with: # Fetch all versions for tag/changelog generation fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v4 with: go-version: 1.24.2 - name: Install promu id: make_promu run: | make promu - name: Calculate Version id: calculate_version uses: mathieudutour/github-tag-action@v4.5 with: github_token: ${{ secrets.GITHUB_TOKEN }} dry_run: true - name: Update Version id: update_version env: NEW_VERSION: ${{ steps.calculate_version.outputs.new_version }} run: | echo "${NEW_VERSION}" > VERSION - name: Update Changelog id: update_changelog env: CHANGELOG: ${{ steps.calculate_version.outputs.changelog }} run: | mv CHANGELOG.md _CHANGELOG.md || touch _CHANGELOG.md echo "${CHANGELOG}" > CHANGELOG.md cat _CHANGELOG.md >> CHANGELOG.md rm -f _CHANGELOG.md - name: Commit Changes id: commit_changes uses: EndBug/add-and-commit@v9.1.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: add: VERSION CHANGELOG.md message: | chore(build): Releasing ${{ steps.calculate_version.outputs.new_tag }} - name: Commit Tag id: commit_tag uses: mathieudutour/github-tag-action@v6.1 with: github_token: ${{ secrets.GITHUB_TOKEN }} commit_sha: ${{ steps.commit_changes.outputs.commit_long_sha }} - name: Build id: build run: | promu crossbuild --parallelism $PARALLELISM promu crossbuild --parallelism $PARALLELISM tarballs promu checksum .tarballs - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ steps.calculate_version.outputs.new_tag }} release_name: Release ${{ steps.calculate_version.outputs.new_tag }} body: | Changes in this release: ${{ steps.calculate_version.outputs.changelog }} draft: false prerelease: false - name: Upload Release Assets id: upload_release_assets uses: AButler/upload-release-assets@v2.0 with: files: ".tarballs/*" repo-token: ${{ secrets.GITHUB_TOKEN }} release-tag: ${{ steps.calculate_version.outputs.new_tag }} ================================================ FILE: .github/workflows/test.yml ================================================ name: Test # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the master branch on: pull_request: branches: - master push: branches: - master # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "test" test: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: Checkout id: checkout uses: actions/checkout@v2 with: # Fetch all versions for tag/changelog generation fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v4 with: go-version: 1.24.2 - name: Test id: test run: | make test ================================================ FILE: .gitignore ================================================ zfs_exporter .build/ .tarballs/ ================================================ FILE: .golangci.yml ================================================ version: "2" linters: enable: - errorlint - misspell - perfsprint - revive - testifylint settings: perfsprint: # Optimizes even if it requires an int or uint type cast. int-conversion: true # Optimizes into `err.Error()` even if it is only equivalent for non-nil errors. err-error: true # Optimizes `fmt.Errorf`. errorf: true # Optimizes `fmt.Sprintf` with only one argument. sprintf1: true # Optimizes into strings concatenation. strconcat: false revive: rules: # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter - name: unused-parameter severity: warning disabled: true testifylint: enable-all: true disable: - go-require formatter: require-f-funcs: true exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling paths: - third_party$ - builtin$ - examples$ issues: max-issues-per-linter: 0 max-same-issues: 0 formatters: enable: - gofumpt - goimports settings: goimports: local-prefixes: - github.com/prometheus/common exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ ================================================ FILE: .promu.yml ================================================ go: # Whenever the Go version is updated here, # .circle/config.yml should also be updated. version: 1.23 repository: path: github.com/pdf/zfs_exporter/v2 build: flags: -a -tags netgo ldflags: | -X github.com/prometheus/common/version.Version={{.Version}} -X github.com/prometheus/common/version.Revision={{.Revision}} -X github.com/prometheus/common/version.Branch={{.Branch}} -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} crossbuild: platforms: - linux - illumos - darwin - freebsd - netbsd - dragonfly tarball: files: - LICENSE - CHANGELOG.md ================================================ FILE: CHANGELOG.md ================================================ ## [2.3.12](https://github.com/pdf/zfs_exporter/compare/v2.3.11...v2.3.12) (2026-04-04) ### Bug Fixes * **docs:** Update installation command for zfs_exporter to v2 ([#66](https://github.com/pdf/zfs_exporter/issues/66)) ([1769a9e](https://github.com/pdf/zfs_exporter/commit/1769a9e)) ## [2.3.11](https://github.com/pdf/zfs_exporter/compare/v2.3.10...v2.3.11) (2025-11-24) ### Bug Fixes * **security:** Bump deps for CVE-2025-58181 ([12cf70c](https://github.com/pdf/zfs_exporter/commit/12cf70c)) ## [2.3.10](https://github.com/pdf/zfs_exporter/compare/v2.3.9...v2.3.10) (2025-08-24) ### Bug Fixes * **props:** Fix filesystem creation property ([9a6beb3](https://github.com/pdf/zfs_exporter/commit/9a6beb3)), closes [#57](https://github.com/pdf/zfs_exporter/issues/57) ## [2.3.9](https://github.com/pdf/zfs_exporter/compare/v2.3.8...v2.3.9) (2025-08-24) ### Bug Fixes * **props:** Add support for dataset `creation` property ([a1c90f4](https://github.com/pdf/zfs_exporter/commit/a1c90f4)), closes [#57](https://github.com/pdf/zfs_exporter/issues/57) ## [2.3.8](https://github.com/pdf/zfs_exporter/compare/v2.3.7...v2.3.8) (2025-04-20) ### Bug Fixes * **build:** Bump Go version and golangci-lint ([4d46ab3](https://github.com/pdf/zfs_exporter/commit/4d46ab3)) ## [2.3.7](https://github.com/pdf/zfs_exporter/compare/v2.3.6...v2.3.7) (2025-04-20) ### Bug Fixes * **deps:** Bump dependencies ([6af54d2](https://github.com/pdf/zfs_exporter/commit/6af54d2)) ## [2.3.6](https://github.com/pdf/zfs_exporter/compare/v2.3.5...v2.3.6) (2025-01-18) ### Bug Fixes * **build:** Bump Go version in actions ([00498df](https://github.com/pdf/zfs_exporter/commit/00498df)) ## [2.3.5](https://github.com/pdf/zfs_exporter/compare/v2.3.4...v2.3.5) (2025-01-18) ### Bug Fixes * **core:** Bump dependencies, migrate to promslog ([ccc2b21](https://github.com/pdf/zfs_exporter/commit/ccc2b21)) ## [2.3.4](https://github.com/pdf/zfs_exporter/compare/v2.3.3...v2.3.4) (2024-04-13) ### Bug Fixes * **deps:** Bump deps for security ([1404536](https://github.com/pdf/zfs_exporter/commit/1404536)) ## [2.3.3](https://github.com/pdf/zfs_exporter/compare/v2.3.2...v2.3.3) (2024-04-13) ### Bug Fixes * **log:** Improve command execution error output ([2277832](https://github.com/pdf/zfs_exporter/commit/2277832)) ## [2.3.2](https://github.com/pdf/zfs_exporter/compare/v2.3.1...v2.3.2) (2023-10-13) ## [2.3.1](https://github.com/pdf/zfs_exporter/compare/v2.3.0...v2.3.1) (2023-08-12) ### Bug Fixes * **build:** Update deps ([ddf8e09](https://github.com/pdf/zfs_exporter/commit/ddf8e09)) # [2.3.0](https://github.com/pdf/zfs_exporter/compare/v2.2.8...v2.3.0) (2023-08-12) ### Features * **server:** Add exporter toolkit for TLS support ([8102e2e](https://github.com/pdf/zfs_exporter/commit/8102e2e)), closes [#34](https://github.com/pdf/zfs_exporter/issues/34) ## [2.2.8](https://github.com/pdf/zfs_exporter/compare/v2.2.7...v2.2.8) (2023-04-22) ### Bug Fixes * **build:** Tag correct commit SHA ([0712333](https://github.com/pdf/zfs_exporter/commit/0712333)) * **security:** Update dependencies for upstream vulnerabilities ([2220da2](https://github.com/pdf/zfs_exporter/commit/2220da2)) ## [2.2.7](https://github.com/pdf/zfs_exporter/compare/v2.2.6...v2.2.7) (2023-01-28) ### Bug Fixes * **transform:** Add support for ancient ZFS dedupratio metric ([85bdc3b](https://github.com/pdf/zfs_exporter/commit/85bdc3b)), closes [#26](https://github.com/pdf/zfs_exporter/issues/26) ## [2.2.6](https://github.com/pdf/zfs_exporter/compare/v2.2.5...v2.2.6) (2023-01-28) ### Bug Fixes * **transform:** Add support for ancient ZFS fragmentation metric ([a0240d1](https://github.com/pdf/zfs_exporter/commit/a0240d1)), closes [#26](https://github.com/pdf/zfs_exporter/issues/26) ## [2.2.5](https://github.com/pdf/zfs_exporter/compare/v2.2.4...v2.2.5) (2022-01-30) ### Bug Fixes * **core:** Correctly handle and report errors listing pools ([efbcceb](https://github.com/pdf/zfs_exporter/commit/efbcceb)), closes [#18](https://github.com/pdf/zfs_exporter/issues/18) ## [2.2.4](https://github.com/pdf/zfs_exporter/compare/v2.2.3...v2.2.4) (2022-01-05) ### Bug Fixes * **build:** Update promu config to build v2 ([2a38914](https://github.com/pdf/zfs_exporter/commit/2a38914)) ## [2.2.3](https://github.com/pdf/zfs_exporter/compare/v2.2.2...v2.2.3) (2022-01-05) ### Bug Fixes * **build:** update go module version to match release tag major version ([f709083](https://github.com/pdf/zfs_exporter/commit/f709083)) ## [2.2.2](https://github.com/pdf/zfs_exporter/compare/v2.2.1...v2.2.2) (2021-11-16) ### Bug Fixes * **metrics:** Fix typo in metric name ([bbd3d91](https://github.com/pdf/zfs_exporter/commit/bbd3d91)) * **pool:** Add SUSPENDED status ([9b9e655](https://github.com/pdf/zfs_exporter/commit/9b9e655)) * **tests:** Remove unnecessary duration conversion ([b6a29ab](https://github.com/pdf/zfs_exporter/commit/b6a29ab)) ## [2.2.1](https://github.com/pdf/zfs_exporter/compare/v2.2.0...v2.2.1) (2021-09-13) ### Bug Fixes * **collector:** Avoid race on upstream channel close, tidy sync points ([e6fbdf5](https://github.com/pdf/zfs_exporter/commit/e6fbdf5)) * **docs:** Document web.disable-exporter-metrics flag in README ([20182da](https://github.com/pdf/zfs_exporter/commit/20182da)) # [2.2.0](https://github.com/pdf/zfs_exporter/compare/v2.1.1...v2.2.0) (2021-09-04) ### Bug Fixes * **docs:** Correct misspelling ([066c7d2](https://github.com/pdf/zfs_exporter/commit/066c7d2)) ### Features * **metrics:** Allow disabling exporter metrics ([1ca8717](https://github.com/pdf/zfs_exporter/commit/1ca8717)), closes [#2](https://github.com/pdf/zfs_exporter/issues/2) ## [2.1.1](https://github.com/pdf/zfs_exporter/compare/v2.1.0...v2.1.1) (2021-08-27) ### Bug Fixes * **build:** Update to Go 1.17 for crossbuild, and enable all platforms ([f47b69a](https://github.com/pdf/zfs_exporter/commit/f47b69a)) * **core:** Update dependencies ([b39382b](https://github.com/pdf/zfs_exporter/commit/b39382b)) # [2.1.0](https://github.com/pdf/zfs_exporter/compare/v2.0.0...v2.1.0) (2021-08-18) ### Bug Fixes * **logging:** Include collector in warning for unsupported properties ([1760a4a](https://github.com/pdf/zfs_exporter/commit/1760a4a)) * **metrics:** Invert ratio for multiplier fields, and clarify their docs ([1a7bc3a](https://github.com/pdf/zfs_exporter/commit/1a7bc3a)), closes [#11](https://github.com/pdf/zfs_exporter/issues/11) ### Features * **build:** Update to Go 1.17 ([b64115c](https://github.com/pdf/zfs_exporter/commit/b64115c)) # [2.0.0](https://github.com/pdf/zfs_exporter/compare/v1.0.1...v2.0.0) (2021-08-14) ### Code Refactoring * **collector:** Migrate to internal ZFS CLI implementation ([53b0e98](https://github.com/pdf/zfs_exporter/commit/53b0e98)), closes [#7](https://github.com/pdf/zfs_exporter/issues/7) [#9](https://github.com/pdf/zfs_exporter/issues/9) [#10](https://github.com/pdf/zfs_exporter/issues/10) ### Features * **performance:** Execute collection concurrently per pool ([ccc6f22](https://github.com/pdf/zfs_exporter/commit/ccc6f22)) * **zfs:** Add local ZFS CLI parsing ([f5050b1](https://github.com/pdf/zfs_exporter/commit/f5050b1)) ### BREAKING CHANGES * **collector:** Ratio values are now properly calculated in the range 0-1, rather than being passed verbatim. The following metrics are affected by this change: - zfs_pool_deduplication_ratio - zfs_pool_capacity_ratio - zfs_pool_fragmentation_ratio - zfs_dataset_compression_ratio - zfs_dataset_referenced_compression_ratio Additionally, the zfs_dataset_fragmentation_percent metric has been renamed to zfs_dataset_fragmentation_ratio. ## [1.0.1](https://github.com/pdf/zfs_exporter/compare/v1.0.0...v1.0.1) (2021-08-03) ### Bug Fixes * fix copy and paste errors when accessing dataset properties ([c0fc6b2](https://github.com/pdf/zfs_exporter/commit/c0fc6b2)) # [1.0.0](https://github.com/pdf/zfs_exporter/compare/v0.0.3...v1.0.0) (2021-06-22) ### Bug Fixes * **ci:** Fix syntax error in github actions workflow ([0b6e8bc](https://github.com/pdf/zfs_exporter/commit/0b6e8bc)) ### Code Refactoring * **core:** Update prometheus toolchain and refactor internals ([056b386](https://github.com/pdf/zfs_exporter/commit/056b386)) ### Features * **enhancement:** Allow excluding datasets by regular expression ([8dd48ba](https://github.com/pdf/zfs_exporter/commit/8dd48ba)), closes [#3](https://github.com/pdf/zfs_exporter/issues/3) ### BREAKING CHANGES * **core:** Go API has changed somewhat, but metrics remain unaffected. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Peter Fern Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ # Copyright 2015 The Prometheus Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Needs to be defined before including Makefile.common to auto-generate targets DOCKER_ARCHS ?= amd64 armv7 arm64 ppc64le s390x DOCKER_IMAGE_NAME ?= zfs-exporter .PHONY: all all:: test build .PHONY: test test:: vet precheck style lint unused common-test include Makefile.common ================================================ FILE: Makefile.common ================================================ # Copyright 2018 The Prometheus Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # A common Makefile that includes rules to be reused in different prometheus projects. # !!! Open PRs only against the prometheus/prometheus/Makefile.common repository! # Example usage : # Create the main Makefile in the root project directory. # include Makefile.common # customTarget: # @echo ">> Running customTarget" # # Ensure GOBIN is not set during build so that promu is installed to the correct path unexport GOBIN GO ?= go GOFMT ?= $(GO)fmt FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) GOOPTS ?= GOHOSTOS ?= $(shell $(GO) env GOHOSTOS) GOHOSTARCH ?= $(shell $(GO) env GOHOSTARCH) GO_VERSION ?= $(shell $(GO) version) GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION)) PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.') PROMU := $(FIRST_GOPATH)/bin/promu pkgs = ./... ifeq (arm, $(GOHOSTARCH)) GOHOSTARM ?= $(shell GOARM= $(GO) env GOARM) GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM) else GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH) endif GOTEST := $(GO) test GOTEST_DIR := ifneq ($(CIRCLE_JOB),) ifneq ($(shell command -v gotestsum 2> /dev/null),) GOTEST_DIR := test-results GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml -- endif endif PROMU_VERSION ?= 0.17.0 PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz SKIP_GOLANGCI_LINT := GOLANGCI_LINT := GOLANGCI_LINT_OPTS ?= GOLANGCI_LINT_VERSION ?= v2.1.2 # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. # windows isn't included here because of the path separator being different. ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64)) # If we're in CI and there is an Actions file, that means the linter # is being run in Actions, so we don't need to run it here. ifneq (,$(SKIP_GOLANGCI_LINT)) GOLANGCI_LINT := else ifeq (,$(CIRCLE_JOB)) GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint else ifeq (,$(wildcard .github/workflows/golangci-lint.yml)) GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint endif endif endif PREFIX ?= $(shell pwd) BIN_DIR ?= $(shell pwd) DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) DOCKERFILE_PATH ?= ./Dockerfile DOCKERBUILD_CONTEXT ?= ./ DOCKER_REPO ?= prom DOCKER_ARCHS ?= amd64 BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS)) PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS)) SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG)) ifeq ($(GOHOSTARCH),amd64) ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows)) # Only supported on amd64 test-flags := -race endif endif # This rule is used to forward a target like "build" to "common-build". This # allows a new "build" target to be defined in a Makefile which includes this # one and override "common-build" without override warnings. %: common-% ; .PHONY: common-all common-all: precheck style check_license lint yamllint unused build test .PHONY: common-style common-style: @echo ">> checking code style" @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ if [ -n "$${fmtRes}" ]; then \ echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ echo "Please ensure you are using $$($(GO) version) for formatting code."; \ exit 1; \ fi .PHONY: common-check_license common-check_license: @echo ">> checking license header" @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \ awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ done); \ if [ -n "$${licRes}" ]; then \ echo "license header checking failed:"; echo "$${licRes}"; \ exit 1; \ fi .PHONY: common-deps common-deps: @echo ">> getting dependencies" $(GO) mod download .PHONY: update-go-deps update-go-deps: @echo ">> updating Go dependencies" @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ $(GO) get -d $$m; \ done $(GO) mod tidy .PHONY: common-test-short common-test-short: $(GOTEST_DIR) @echo ">> running short tests" $(GOTEST) -short $(GOOPTS) $(pkgs) .PHONY: common-test common-test: $(GOTEST_DIR) @echo ">> running all tests" $(GOTEST) $(test-flags) $(GOOPTS) $(pkgs) $(GOTEST_DIR): @mkdir -p $@ .PHONY: common-format common-format: @echo ">> formatting code" $(GO) fmt $(pkgs) .PHONY: common-vet common-vet: @echo ">> vetting code" $(GO) vet $(GOOPTS) $(pkgs) .PHONY: common-lint common-lint: $(GOLANGCI_LINT) ifdef GOLANGCI_LINT @echo ">> running golangci-lint" $(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs) endif .PHONY: common-lint-fix common-lint-fix: $(GOLANGCI_LINT) ifdef GOLANGCI_LINT @echo ">> running golangci-lint fix" $(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs) endif .PHONY: common-yamllint common-yamllint: @echo ">> running yamllint on all YAML files in the repository" ifeq (, $(shell command -v yamllint 2> /dev/null)) @echo "yamllint not installed so skipping" else yamllint . endif # For backward-compatibility. .PHONY: common-staticcheck common-staticcheck: lint .PHONY: common-unused common-unused: @echo ">> running check for unused/missing packages in go.mod" $(GO) mod tidy @git diff --exit-code -- go.sum go.mod .PHONY: common-build common-build: promu @echo ">> building binaries" $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) .PHONY: common-tarball common-tarball: promu @echo ">> building release tarball" $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) .PHONY: common-docker-repo-name common-docker-repo-name: @echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)" .PHONY: common-docker $(BUILD_DOCKER_ARCHS) common-docker: $(BUILD_DOCKER_ARCHS) $(BUILD_DOCKER_ARCHS): common-docker-%: docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ -f $(DOCKERFILE_PATH) \ --build-arg ARCH="$*" \ --build-arg OS="linux" \ $(DOCKERBUILD_CONTEXT) .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) common-docker-publish: $(PUBLISH_DOCKER_ARCHS) $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION))) .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) common-docker-tag-latest: $(TAG_DOCKER_ARCHS) $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest" docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)" .PHONY: common-docker-manifest common-docker-manifest: DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(SANITIZED_DOCKER_IMAGE_TAG)) DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" .PHONY: promu promu: $(PROMU) $(PROMU): $(eval PROMU_TMP := $(shell mktemp -d)) curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) mkdir -p $(FIRST_GOPATH)/bin cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu rm -r $(PROMU_TMP) .PHONY: proto proto: @echo ">> generating code from proto files" @./scripts/genproto.sh ifdef GOLANGCI_LINT $(GOLANGCI_LINT): mkdir -p $(FIRST_GOPATH)/bin curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \ | sed -e '/install -d/d' \ | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) endif .PHONY: precheck precheck:: define PRECHECK_COMMAND_template = precheck:: $(1)_precheck PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1))) .PHONY: $(1)_precheck $(1)_precheck: @if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \ echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \ exit 1; \ fi endef govulncheck: install-govulncheck govulncheck ./... install-govulncheck: command -v govulncheck > /dev/null || go install golang.org/x/vuln/cmd/govulncheck@latest ================================================ FILE: README.md ================================================ # ZFS Exporter [![Test](https://github.com/pdf/zfs_exporter/actions/workflows/test.yml/badge.svg)](https://github.com/pdf/zfs_exporter/actions/workflows/test.yml) [![Release](https://github.com/pdf/zfs_exporter/actions/workflows/release.yml/badge.svg)](https://github.com/pdf/zfs_exporter/actions/workflows/release.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/pdf/zfs_exporter)](https://goreportcard.com/report/github.com/pdf/zfs_exporter) [![License](https://img.shields.io/badge/License-MIT-%23a31f34)](https://github.com/pdf/zfs_exporter/blob/master/LICENSE) Prometheus exporter for ZFS (pools, filesystems, snapshots and volumes). Other implementations exist, however performance can be quite variable, producing occasional timeouts (and associated alerts). This exporter was built with a few features aimed at allowing users to avoid collecting more than they need to, and to ensure timeouts cannot occur, but that we eventually return useful data: - **Pool selection** - allow the user to select which pools are collected - **Multiple collectors** - allow the user to select which data types are collected (pools, filesystems, snapshots and volumes) - **Property selection** - allow the user to select which properties are collected per data type (enabling only required properties will increase collector performance, by reducing metadata queries) - **Collection deadline and caching** - if the collection duration exceeds the configured deadline, cached data from the last run will be returned for any metrics that have not yet been collected, and the current collection run will continue in the background. Collections will not run concurrently, so that when a system is running slowly, we don't compound the problem - if an existing collection is still running, cached data will be returned. ## Installation Download the [latest release](https://github.com/pdf/zfs_exporter/releases/latest) for your platform, and unpack it somewhere on your filesystem. You may also build the latest version using Go v1.11 - 1.17 via `go get`: ```bash go get -u github.com/pdf/zfs_exporter ``` Installation can also be accomplished using `go install`: ```bash version=latest # or a specific version tag go install github.com/pdf/zfs_exporter/v2@$version ``` ## Usage ``` usage: zfs_exporter [] Flags: -h, --[no-]help Show context-sensitive help (also try --help-long and --help-man). --[no-]collector.dataset-filesystem Enable the dataset-filesystem collector (default: enabled) --properties.dataset-filesystem="available,logicalused,quota,referenced,used,usedbydataset,written" Properties to include for the dataset-filesystem collector, comma-separated. --[no-]collector.dataset-snapshot Enable the dataset-snapshot collector (default: disabled) --properties.dataset-snapshot="logicalused,referenced,used,written" Properties to include for the dataset-snapshot collector, comma-separated. --[no-]collector.dataset-volume Enable the dataset-volume collector (default: enabled) --properties.dataset-volume="available,logicalused,referenced,used,usedbydataset,volsize,written" Properties to include for the dataset-volume collector, comma-separated. --[no-]collector.pool Enable the pool collector (default: enabled) --properties.pool="allocated,dedupratio,fragmentation,free,freeing,health,leaked,readonly,size" Properties to include for the pool collector, comma-separated. --web.telemetry-path="/metrics" Path under which to expose metrics. --[no-]web.disable-exporter-metrics Exclude metrics about the exporter itself (promhttp_*, process_*, go_*). --deadline=8s Maximum duration that a collection should run before returning cached data. Should be set to a value shorter than your scrape timeout duration. The current collection run will continue and update the cache when complete (default: 8s) --pool=POOL ... Name of the pool(s) to collect, repeat for multiple pools (default: all pools). --exclude=EXCLUDE ... Exclude datasets/snapshots/volumes that match the provided regex (e.g. '^rpool/docker/'), may be specified multiple times. --[no-]web.systemd-socket Use systemd socket activation listeners instead of port listeners (Linux only). --web.listen-address=:9134 ... Addresses on which to expose metrics and web interface. Repeatable for multiple addresses. Examples: `:9100` or `[::1]:9100` for http, `vsock://:9100` for vsock --web.config.file="" Path to configuration file that can enable TLS or authentication. See: https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md --log.level=info Only log messages with the given severity or above. One of: [debug, info, warn, error] --log.format=logfmt Output format of log messages. One of: [logfmt, json] --[no-]version Show application version. ``` Collectors that are enabled by default can be negated by prefixing the flag with `--no-*`, ie: ``` zfs_exporter --no-collector.dataset-filesystem ``` ## TLS endpoint **EXPERIMENTAL** The exporter supports TLS via a new web configuration file. ```console ./zfs_exporter --web.config.file=web-config.yml ``` See the [exporter-toolkit https package](https://github.com/prometheus/exporter-toolkit/blob/v0.1.0/https/README.md) for more details. ## Caveats The collector may need to be run as root on some platforms (ie - Linux prior to ZFS v0.7.0). Whilst inspiration was taken from some of the alternative ZFS collectors, metric names may not be compatible. ## Alternatives In no particular order, here are some alternative implementations: - https://github.com/eliothedeman/zfs_exporter - https://github.com/ncabatoff/zfs-exporter - https://github.com/eripa/prometheus-zfs ================================================ FILE: VERSION ================================================ 2.3.12 ================================================ FILE: collector/cache.go ================================================ package collector import ( "sync" "github.com/prometheus/client_golang/prometheus" ) type metricCache struct { cache map[string]prometheus.Metric sync.RWMutex } func (c *metricCache) add(m metric) { c.Lock() defer c.Unlock() c.cache[m.name] = m.prometheus } func (c *metricCache) merge(other *metricCache) { if c == other { return } c.Lock() other.RLock() defer func() { other.RUnlock() c.Unlock() }() for name, value := range other.cache { c.cache[name] = value } } func (c *metricCache) replace(other *metricCache) { c.Lock() defer c.Unlock() c.cache = other.cache } func (c *metricCache) index() map[string]struct{} { c.RLock() defer c.RUnlock() index := make(map[string]struct{}, len(c.cache)) for name := range c.cache { index[name] = struct{}{} } return index } func newMetricCache() *metricCache { return &metricCache{cache: make(map[string]prometheus.Metric)} } ================================================ FILE: collector/collector.go ================================================ package collector import ( "errors" "fmt" "log/slog" "strconv" "strings" "github.com/alecthomas/kingpin/v2" "github.com/pdf/zfs_exporter/v2/zfs" "github.com/prometheus/client_golang/prometheus" ) const ( defaultEnabled = true defaultDisabled = false namespace = `zfs` helpDefaultStateEnabled = `enabled` helpDefaultStateDisabled = `disabled` subsystemDataset = `dataset` subsystemPool = `pool` propertyUnsupportedDesc = `!!! This property is unsupported, results are likely to be undesirable, please file an issue at https://github.com/pdf/zfs_exporter/issues to have this property supported !!!` propertyUnsupportedMsg = `Unsupported dataset property, results are likely to be undesirable` helpIssue = `Please file an issue at https://github.com/pdf/zfs_exporter/issues` ) var ( collectorStates = make(map[string]State) scrapeDurationDescName = prometheus.BuildFQName(namespace, `scrape`, `collector_duration_seconds`) scrapeDurationDesc = prometheus.NewDesc( scrapeDurationDescName, `zfs_exporter: Duration of a collector scrape.`, []string{`collector`}, nil, ) scrapeSuccessDescName = prometheus.BuildFQName(namespace, `scrape`, `collector_success`) scrapeSuccessDesc = prometheus.NewDesc( scrapeSuccessDescName, `zfs_exporter: Whether a collector succeeded.`, []string{`collector`}, nil, ) errUnsupportedProperty = errors.New(`unsupported property`) ) type factoryFunc func(l *slog.Logger, c zfs.Client, properties []string) (Collector, error) type transformFunc func(string) (float64, error) // State holds metadata for managing collector status type State struct { Name string Enabled *bool Properties *string factory factoryFunc } // Collector defines the minimum functionality for registering a collector type Collector interface { update(ch chan<- metric, pools []string, excludes regexpCollection) error describe(ch chan<- *prometheus.Desc) } type metric struct { name string prometheus prometheus.Metric } type property struct { name string desc *prometheus.Desc transform transformFunc kind prometheus.ValueType } func (p property) push(ch chan<- metric, value string, labelValues ...string) error { v, err := p.transform(value) if err != nil { return err } ch <- metric{ name: expandMetricName(p.name, labelValues...), prometheus: prometheus.MustNewConstMetric( p.desc, p.kind, v, labelValues..., ), } return nil } type propertyStore struct { defaultSubsystem string defaultLabels []string store map[string]property } func (p *propertyStore) find(name string) (property, error) { prop, ok := p.store[name] if !ok { prop = newProperty( p.defaultSubsystem, name, propertyUnsupportedDesc, transformNumeric, prometheus.GaugeValue, p.defaultLabels..., ) return prop, errUnsupportedProperty } return prop, nil } func registerCollector(collector string, isDefaultEnabled bool, defaultProps string, factory factoryFunc) { helpDefaultState := helpDefaultStateDisabled if isDefaultEnabled { helpDefaultState = helpDefaultStateEnabled } enabledFlagName := fmt.Sprintf("collector.%s", collector) enabledFlagHelp := fmt.Sprintf("Enable the %s collector (default: %s)", collector, helpDefaultState) enabledDefaultValue := strconv.FormatBool(isDefaultEnabled) propsFlagName := fmt.Sprintf("properties.%s", collector) propsFlagHelp := fmt.Sprintf("Properties to include for the %s collector, comma-separated.", collector) enabledFlag := kingpin.Flag(enabledFlagName, enabledFlagHelp).Default(enabledDefaultValue).Bool() propsFlag := kingpin.Flag(propsFlagName, propsFlagHelp).Default(defaultProps).String() collectorStates[collector] = State{ Enabled: enabledFlag, Properties: propsFlag, factory: factory, } } func expandMetricName(prefix string, context ...string) string { return strings.Join(append(context, prefix), `-`) } func newProperty(subsystem, metricName, helpText string, transform transformFunc, kind prometheus.ValueType, labels ...string) property { name := prometheus.BuildFQName(namespace, subsystem, metricName) return property{ name: name, desc: prometheus.NewDesc(name, helpText, labels, nil), transform: transform, kind: kind, } } ================================================ FILE: collector/collector_test.go ================================================ package collector import ( "bytes" "context" "io" "log/slog" "time" "github.com/pdf/zfs_exporter/v2/zfs" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" ) var logger = slog.New(slog.NewTextHandler(io.Discard, nil)) func callCollector(ctx context.Context, collector prometheus.Collector, metricResults []byte, metricNames []string) error { result := make(chan error) go func() { result <- testutil.CollectAndCompare(collector, bytes.NewBuffer(metricResults), metricNames...) }() select { case err := <-result: return err case <-ctx.Done(): return ctx.Err() } } func defaultConfig(z zfs.Client) ZFSConfig { return ZFSConfig{ DisableMetrics: true, Deadline: 5 * time.Minute, Logger: logger, ZFSClient: z, } } func stringPointer(s string) *string { return &s } func boolPointer(b bool) *bool { return &b } ================================================ FILE: collector/dataset.go ================================================ package collector import ( "fmt" "log/slog" "sync" "github.com/pdf/zfs_exporter/v2/zfs" "github.com/prometheus/client_golang/prometheus" ) const ( defaultFilesystemProps = `available,logicalused,quota,referenced,used,usedbydataset,written` defaultSnapshotProps = `logicalused,referenced,used,written` defaultVolumeProps = `available,logicalused,referenced,used,usedbydataset,volsize,written` ) var ( datasetLabels = []string{`name`, `pool`, `type`} datasetProperties = propertyStore{ defaultSubsystem: subsystemDataset, defaultLabels: datasetLabels, store: map[string]property{ `available`: newProperty( subsystemDataset, `available_bytes`, `The amount of space in bytes available to the dataset and all its children.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `compressratio`: newProperty( subsystemDataset, `compression_ratio`, `The ratio of compressed size vs uncompressed size for this dataset.`, transformMultiplier, prometheus.GaugeValue, datasetLabels..., ), `logicalused`: newProperty( subsystemDataset, `logical_used_bytes`, `The amount of space in bytes that is "logically" consumed by this dataset and all its descendents. See the "used_bytes" property.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `logicalreferenced`: newProperty( subsystemDataset, `logical_referenced_bytes`, `The amount of space that is "logically" accessible by this dataset. See the "referenced_bytes" property.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `quota`: newProperty( subsystemDataset, `quota_bytes`, `The maximum amount of space in bytes this dataset and its descendents can consume.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `refcompressratio`: newProperty( subsystemDataset, `referenced_compression_ratio`, `The ratio of compressed size vs uncompressed size for the referenced space of this dataset. See also the "compression_ratio" property.`, transformMultiplier, prometheus.GaugeValue, datasetLabels..., ), `referenced`: newProperty( subsystemDataset, `referenced_bytes`, `The amount of data in bytes that is accessible by this dataset, which may or may not be shared with other datasets in the pool.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `refquota`: newProperty( subsystemDataset, `referenced_quota_bytes`, `The maximum amount of space in bytes this dataset can consume.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `refreservation`: newProperty( subsystemDataset, `referenced_reservation_bytes`, `The minimum amount of space in bytes guaranteed to this dataset.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `reservation`: newProperty( subsystemDataset, `reservation_bytes`, `The minimum amount of space in bytes guaranteed to a dataset and its descendants.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `snapshot_count`: newProperty( subsystemDataset, `snapshot_count_total`, `The total number of snapshots that exist under this location in the dataset tree. This value is only available when a snapshot_limit has been set somewhere in the tree under which the dataset resides.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `snapshot_limit`: newProperty( subsystemDataset, `snapshot_limit_total`, `The total limit on the number of snapshots that can be created on a dataset and its descendents.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `used`: newProperty( subsystemDataset, `used_bytes`, `The amount of space in bytes consumed by this dataset and all its descendents.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `usedbychildren`: newProperty( subsystemDataset, `used_by_children_bytes`, `The amount of space in bytes used by children of this dataset, which would be freed if all the dataset's children were destroyed.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `usedbydataset`: newProperty( subsystemDataset, `used_by_dataset_bytes`, `The amount of space in bytes used by this dataset itself, which would be freed if the dataset were destroyed.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `usedbyrefreservation`: newProperty( subsystemDataset, `used_by_referenced_reservation_bytes`, `The amount of space in bytes used by a refreservation set on this dataset, which would be freed if the refreservation was removed.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `usedbysnapshots`: newProperty( subsystemDataset, `used_by_snapshot_bytes`, `The amount of space in bytes consumed by snapshots of this dataset.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `volsize`: newProperty( subsystemDataset, `volume_size_bytes`, `The logical size in bytes of this volume.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `written`: newProperty( subsystemDataset, `written_bytes`, `The amount of referenced space in bytes written to this dataset since the previous snapshot.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), `creation`: newProperty( subsystemDataset, `creation_timestamp`, `The unix timestamp when this dataset was created.`, transformNumeric, prometheus.GaugeValue, datasetLabels..., ), }, } ) func init() { registerCollector(`dataset-filesystem`, defaultEnabled, defaultFilesystemProps, newFilesystemCollector) registerCollector(`dataset-snapshot`, defaultDisabled, defaultSnapshotProps, newSnapshotCollector) registerCollector(`dataset-volume`, defaultEnabled, defaultVolumeProps, newVolumeCollector) } type datasetCollector struct { kind zfs.DatasetKind log *slog.Logger client zfs.Client props []string } func (c *datasetCollector) describe(ch chan<- *prometheus.Desc) { for _, k := range c.props { prop, err := datasetProperties.find(k) if err != nil { c.log.Warn(propertyUnsupportedMsg, `help`, helpIssue, `collector`, c.kind, `property`, k, `err`, err) continue } ch <- prop.desc } } func (c *datasetCollector) update(ch chan<- metric, pools []string, excludes regexpCollection) error { var wg sync.WaitGroup errChan := make(chan error, len(pools)) for _, pool := range pools { wg.Add(1) go func(pool string) { if err := c.updatePoolMetrics(ch, pool, excludes); err != nil { errChan <- err } wg.Done() }(pool) } wg.Wait() select { case err := <-errChan: return err default: return nil } } func (c *datasetCollector) updatePoolMetrics(ch chan<- metric, pool string, excludes regexpCollection) error { datasets := c.client.Datasets(pool, c.kind) props, err := datasets.Properties(c.props...) if err != nil { return err } for _, dataset := range props { if excludes.MatchString(dataset.DatasetName()) { continue } if err = c.updateDatasetMetrics(ch, pool, dataset); err != nil { return err } } return nil } func (c *datasetCollector) updateDatasetMetrics(ch chan<- metric, pool string, dataset zfs.DatasetProperties) error { labelValues := []string{dataset.DatasetName(), pool, string(c.kind)} for k, v := range dataset.Properties() { prop, err := datasetProperties.find(k) if err != nil { c.log.Warn(propertyUnsupportedMsg, `help`, helpIssue, `collector`, c.kind, `property`, k, `err`, err) } if err = prop.push(ch, v, labelValues...); err != nil { return err } } return nil } func newDatasetCollector(kind zfs.DatasetKind, l *slog.Logger, c zfs.Client, props []string) (Collector, error) { switch kind { case zfs.DatasetFilesystem, zfs.DatasetSnapshot, zfs.DatasetVolume: default: return nil, fmt.Errorf("unknown dataset type: %s", kind) } return &datasetCollector{kind: kind, log: l, client: c, props: props}, nil } func newFilesystemCollector(l *slog.Logger, c zfs.Client, props []string) (Collector, error) { return newDatasetCollector(zfs.DatasetFilesystem, l, c, props) } func newSnapshotCollector(l *slog.Logger, c zfs.Client, props []string) (Collector, error) { return newDatasetCollector(zfs.DatasetSnapshot, l, c, props) } func newVolumeCollector(l *slog.Logger, c zfs.Client, props []string) (Collector, error) { return newDatasetCollector(zfs.DatasetVolume, l, c, props) } ================================================ FILE: collector/dataset_test.go ================================================ package collector import ( "context" "strings" "testing" "github.com/pdf/zfs_exporter/v2/zfs" "github.com/pdf/zfs_exporter/v2/zfs/mock_zfs" "go.uber.org/mock/gomock" ) type datasetResults struct { name string results map[string]string } func TestDatsetMetrics(t *testing.T) { testCases := []struct { name string kinds []zfs.DatasetKind pools []string explicitPools []string propsRequested []string metricNames []string propsResults map[string][]datasetResults metricResults string }{ { name: `all metrics`, kinds: []zfs.DatasetKind{zfs.DatasetFilesystem}, pools: []string{`testpool`}, propsRequested: []string{`available`, `compressratio`, `logicalused`, `logicalreferenced`, `quota`, `refcompressratio`, `referenced`, `refquota`, `refreservation`, `reservation`, `snapshot_count`, `snapshot_limit`, `used`, `usedbychildren`, `usedbydataset`, `usedbyrefreservation`, `usedbysnapshots`, `volsize`, `written`, `creation`}, metricNames: []string{`zfs_dataset_available_bytes`, `zfs_dataset_compression_ratio`, `zfs_dataset_logical_used_bytes`, `zfs_dataset_logical_referenced_bytes`, `zfs_dataset_quota_bytes`, `zfs_dataset_referenced_compression_ratio`, `zfs_dataset_referenced_bytes`, `zfs_dataset_referenced_quota_bytes`, `zfs_dataset_reservation_bytes`, `zfs_dataset_snapshot_count_total`, `zfs_datset_snapshot_limit_total`, `zfs_dataset_used_bytes`, `zfs_dataset_used_by_children_bytes`, `zfs_dataset_used_by_datset_bytes`, `zfs_datset_used_by_referenced_reservation_bytes`, `zfs_dataset_used_by_snapshot_bytes`, `zfs_dataset_volume_size_bytes`, `zfs_dataset_written_bytes`}, propsResults: map[string][]datasetResults{ `testpool`: { { name: `testpool/test`, results: map[string]string{ `available`: `1024`, `compressratio`: `2.50`, `logicalused`: `1024`, `logicalreferenced`: `512`, `quota`: `512`, `refcompressratio`: `24.00`, `referenced`: `1024`, `refreservation`: `1024`, `reservation`: `1024`, `snapshot_count`: `12`, `snapshot_limit`: `24`, `used`: `1024`, `usedbychildren`: `1024`, `usedbydataset`: `1024`, `usedbyrefreservation`: `1024`, `usedbysnapshots`: `1024`, `volsize`: `1024`, `written`: `1024`, `creation`: `1756033110`, }, }, }, }, metricResults: `# HELP zfs_dataset_available_bytes The amount of space in bytes available to the dataset and all its children. # TYPE zfs_dataset_available_bytes gauge zfs_dataset_available_bytes{name="testpool/test",pool="testpool",type="filesystem"} 1024 # HELP zfs_dataset_compression_ratio The ratio of compressed size vs uncompressed size for this dataset. # TYPE zfs_dataset_compression_ratio gauge zfs_dataset_compression_ratio{name="testpool/test",pool="testpool",type="filesystem"} 0.4 # HELP zfs_dataset_logical_used_bytes The amount of space in bytes that is "logically" consumed by this dataset and all its descendents. See the "used_bytes" property. # TYPE zfs_dataset_logical_used_bytes gauge zfs_dataset_logical_used_bytes{name="testpool/test",pool="testpool",type="filesystem"} 1024 # HELP zfs_dataset_logical_referenced_bytes The amount of space that is "logically" accessible by this dataset. See the "referenced_bytes" property. # TYPE zfs_dataset_logical_referenced_bytes gauge zfs_dataset_logical_referenced_bytes{name="testpool/test",pool="testpool",type="filesystem"} 512 # HELP zfs_dataset_quota_bytes The maximum amount of space in bytes this dataset and its descendents can consume. # TYPE zfs_dataset_quota_bytes gauge zfs_dataset_quota_bytes{name="testpool/test",pool="testpool",type="filesystem"} 512 # HELP zfs_dataset_referenced_bytes The amount of data in bytes that is accessible by this dataset, which may or may not be shared with other datasets in the pool. # TYPE zfs_dataset_referenced_bytes gauge zfs_dataset_referenced_bytes{name="testpool/test",pool="testpool",type="filesystem"} 1024 # HELP zfs_dataset_referenced_compression_ratio The ratio of compressed size vs uncompressed size for the referenced space of this dataset. See also the "compression_ratio" property. # TYPE zfs_dataset_referenced_compression_ratio gauge zfs_dataset_referenced_compression_ratio{name="testpool/test",pool="testpool",type="filesystem"} 0.041666666666666664 # HELP zfs_dataset_reservation_bytes The minimum amount of space in bytes guaranteed to a dataset and its descendants. # TYPE zfs_dataset_reservation_bytes gauge zfs_dataset_reservation_bytes{name="testpool/test",pool="testpool",type="filesystem"} 1024 # HELP zfs_dataset_snapshot_count_total The total number of snapshots that exist under this location in the dataset tree. This value is only available when a snapshot_limit has been set somewhere in the tree under which the dataset resides. # TYPE zfs_dataset_snapshot_count_total gauge zfs_dataset_snapshot_count_total{name="testpool/test",pool="testpool",type="filesystem"} 12 # HELP zfs_dataset_used_by_children_bytes The amount of space in bytes used by children of this dataset, which would be freed if all the dataset's children were destroyed. # TYPE zfs_dataset_used_by_children_bytes gauge zfs_dataset_used_by_children_bytes{name="testpool/test",pool="testpool",type="filesystem"} 1024 # HELP zfs_dataset_used_by_snapshot_bytes The amount of space in bytes consumed by snapshots of this dataset. # TYPE zfs_dataset_used_by_snapshot_bytes gauge zfs_dataset_used_by_snapshot_bytes{name="testpool/test",pool="testpool",type="filesystem"} 1024 # HELP zfs_dataset_used_bytes The amount of space in bytes consumed by this dataset and all its descendents. # TYPE zfs_dataset_used_bytes gauge zfs_dataset_used_bytes{name="testpool/test",pool="testpool",type="filesystem"} 1024 # HELP zfs_dataset_volume_size_bytes The logical size in bytes of this volume. # TYPE zfs_dataset_volume_size_bytes gauge zfs_dataset_volume_size_bytes{name="testpool/test",pool="testpool",type="filesystem"} 1024 # HELP zfs_dataset_written_bytes The amount of referenced space in bytes written to this dataset since the previous snapshot. # TYPE zfs_dataset_written_bytes gauge zfs_dataset_written_bytes{name="testpool/test",pool="testpool",type="filesystem"} 1024 # HELP zfs_dataset_creation_timestamp The unix timestamp when this dataset was created. # TYPE zfs_dataset_creation_timestamp gauge zfs_dataset_creation_timestamp{name="testpool/test",pool="testpool",type="filesystem"} 1756033110 `, }, { name: `multiple pools`, kinds: []zfs.DatasetKind{zfs.DatasetFilesystem}, pools: []string{`testpool1`, `testpool2`}, propsRequested: []string{`available`}, metricNames: []string{`zfs_dataset_available_bytes`}, propsResults: map[string][]datasetResults{ `testpool1`: { { name: `testpool1/test`, results: map[string]string{ `available`: `1024`, }, }, }, `testpool2`: { { name: `testpool2/test`, results: map[string]string{ `available`: `1024`, }, }, }, }, metricResults: `# HELP zfs_dataset_available_bytes The amount of space in bytes available to the dataset and all its children. # TYPE zfs_dataset_available_bytes gauge zfs_dataset_available_bytes{name="testpool1/test",pool="testpool1",type="filesystem"} 1024 zfs_dataset_available_bytes{name="testpool2/test",pool="testpool2",type="filesystem"} 1024 `, }, { name: `explicit pools`, kinds: []zfs.DatasetKind{zfs.DatasetFilesystem}, pools: []string{`testpool1`, `testpool2`}, explicitPools: []string{`testpool1`}, propsRequested: []string{`available`}, metricNames: []string{`zfs_dataset_available_bytes`}, propsResults: map[string][]datasetResults{ `testpool1`: { { name: `testpool1/test`, results: map[string]string{ `available`: `1024`, }, }, }, `testpool2`: { { name: `testpool2/test`, results: map[string]string{ `available`: `1024`, }, }, }, }, metricResults: `# HELP zfs_dataset_available_bytes The amount of space in bytes available to the dataset and all its children. # TYPE zfs_dataset_available_bytes gauge zfs_dataset_available_bytes{name="testpool1/test",pool="testpool1",type="filesystem"} 1024 `, }, { name: `multiple collectors`, kinds: []zfs.DatasetKind{zfs.DatasetFilesystem, zfs.DatasetSnapshot, zfs.DatasetVolume}, pools: []string{`testpool`}, propsRequested: []string{`available`}, metricNames: []string{`zfs_dataset_available_bytes`}, propsResults: map[string][]datasetResults{ `testpool`: { { name: `testpool/test`, results: map[string]string{ `available`: `1024`, }, }, }, }, metricResults: `# HELP zfs_dataset_available_bytes The amount of space in bytes available to the dataset and all its children. # TYPE zfs_dataset_available_bytes gauge zfs_dataset_available_bytes{name="testpool/test",pool="testpool",type="filesystem"} 1024 zfs_dataset_available_bytes{name="testpool/test",pool="testpool",type="snapshot"} 1024 zfs_dataset_available_bytes{name="testpool/test",pool="testpool",type="volume"} 1024 `, }, { name: `unsupported metric`, kinds: []zfs.DatasetKind{zfs.DatasetFilesystem}, pools: []string{`testpool`}, propsRequested: []string{`unsupported`}, metricNames: []string{`zfs_dataset_unsupported`}, propsResults: map[string][]datasetResults{ `testpool`: { { name: `testpool/test`, results: map[string]string{ `unsupported`: `1024`, }, }, }, }, metricResults: `# HELP zfs_dataset_unsupported !!! This property is unsupported, results are likely to be undesirable, please file an issue at https://github.com/pdf/zfs_exporter/issues to have this property supported !!! # TYPE zfs_dataset_unsupported gauge zfs_dataset_unsupported{name="testpool/test",pool="testpool",type="filesystem"} 1024 `, }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() ctrl, ctx := gomock.WithContext(context.Background(), t) zfsClient := mock_zfs.NewMockClient(ctrl) config := defaultConfig(zfsClient) if tc.explicitPools != nil { config.Pools = tc.explicitPools } zfsClient.EXPECT().PoolNames().Return(tc.pools, nil).Times(1) collector, err := NewZFS(config) if err != nil { t.Fatal(err) } collector.Collectors = make(map[string]State) for _, kind := range tc.kinds { switch kind { case zfs.DatasetFilesystem: collector.Collectors[`dataset-filesystem`] = State{ Name: "dataset-filesystem", Enabled: boolPointer(true), Properties: stringPointer(strings.Join(tc.propsRequested, `,`)), factory: newFilesystemCollector, } case zfs.DatasetSnapshot: collector.Collectors[`dataset-snapshot`] = State{ Name: "dataset-snapshot", Enabled: boolPointer(true), Properties: stringPointer(strings.Join(tc.propsRequested, `,`)), factory: newSnapshotCollector, } case zfs.DatasetVolume: collector.Collectors[`dataset-volume`] = State{ Name: "dataset-volume", Enabled: boolPointer(true), Properties: stringPointer(strings.Join(tc.propsRequested, `,`)), factory: newVolumeCollector, } } for _, pool := range tc.pools { if tc.explicitPools != nil { wanted := false for _, explicit := range tc.explicitPools { if pool == explicit { wanted = true } break } if !wanted { continue } } zfsDatasetResults := make([]zfs.DatasetProperties, len(tc.propsResults[pool])) for i, propResults := range tc.propsResults[pool] { zfsDatasetProperties := mock_zfs.NewMockDatasetProperties(ctrl) zfsDatasetProperties.EXPECT().DatasetName().Return(propResults.name).Times(2) zfsDatasetProperties.EXPECT().Properties().Return(propResults.results).Times(1) zfsDatasetResults[i] = zfsDatasetProperties } zfsDatasets := mock_zfs.NewMockDatasets(ctrl) zfsDatasets.EXPECT().Properties(tc.propsRequested).Return(zfsDatasetResults, nil).Times(1) zfsClient.EXPECT().Datasets(pool, kind).Return(zfsDatasets).Times(1) } } if err = callCollector(ctx, collector, []byte(tc.metricResults), tc.metricNames); err != nil { t.Fatal(err) } }) } } ================================================ FILE: collector/pool.go ================================================ package collector import ( "fmt" "log/slog" "sync" "github.com/pdf/zfs_exporter/v2/zfs" "github.com/prometheus/client_golang/prometheus" ) const ( defaultPoolProps = `allocated,dedupratio,fragmentation,free,freeing,health,leaked,readonly,size` ) var ( poolLabels = []string{`pool`} poolProperties = propertyStore{ defaultSubsystem: subsystemPool, defaultLabels: poolLabels, store: map[string]property{ `allocated`: newProperty( subsystemPool, `allocated_bytes`, `Amount of storage in bytes used within the pool.`, transformNumeric, prometheus.GaugeValue, poolLabels..., ), `dedupratio`: newProperty( subsystemPool, `deduplication_ratio`, `The ratio of deduplicated size vs undeduplicated size for data in this pool.`, transformMultiplier, prometheus.GaugeValue, poolLabels..., ), `capacity`: newProperty( subsystemPool, `capacity_ratio`, `Ratio of pool space used.`, transformPercentage, prometheus.GaugeValue, poolLabels..., ), `expandsize`: newProperty( subsystemPool, `expand_size_bytes`, `Amount of uninitialized space within the pool or device that can be used to increase the total capacity of the pool.`, transformNumeric, prometheus.GaugeValue, poolLabels..., ), `fragmentation`: newProperty( subsystemPool, `fragmentation_ratio`, `The fragmentation ratio of the pool.`, transformPercentage, prometheus.GaugeValue, poolLabels..., ), `free`: newProperty( subsystemPool, `free_bytes`, `The amount of free space in bytes available in the pool.`, transformNumeric, prometheus.GaugeValue, poolLabels..., ), `freeing`: newProperty( subsystemPool, `freeing_bytes`, `The amount of space in bytes remaining to be freed following the destruction of a file system or snapshot.`, transformNumeric, prometheus.GaugeValue, poolLabels..., ), `health`: newProperty( subsystemPool, `health`, fmt.Sprintf("Health status code for the pool [%d: %s, %d: %s, %d: %s, %d: %s, %d: %s, %d: %s, %d: %s].", poolOnline, zfs.PoolOnline, poolDegraded, zfs.PoolDegraded, poolFaulted, zfs.PoolFaulted, poolOffline, zfs.PoolOffline, poolUnavail, zfs.PoolUnavail, poolRemoved, zfs.PoolRemoved, poolSuspended, zfs.PoolSuspended, ), transformHealthCode, prometheus.GaugeValue, poolLabels..., ), `leaked`: newProperty( subsystemPool, `leaked_bytes`, `Number of leaked bytes in the pool.`, transformNumeric, prometheus.GaugeValue, poolLabels..., ), `readonly`: newProperty( subsystemPool, `readonly`, `Read-only status of the pool [0: read-write, 1: read-only].`, transformBool, prometheus.GaugeValue, poolLabels..., ), `size`: newProperty( subsystemPool, `size_bytes`, `Total size in bytes of the storage pool.`, transformNumeric, prometheus.GaugeValue, poolLabels..., ), }, } ) func init() { registerCollector(`pool`, defaultEnabled, defaultPoolProps, newPoolCollector) } type poolCollector struct { log *slog.Logger client zfs.Client props []string } func (c *poolCollector) describe(ch chan<- *prometheus.Desc) { for _, k := range c.props { prop, err := poolProperties.find(k) if err != nil { c.log.Warn(propertyUnsupportedMsg, `help`, helpIssue, `collector`, `pool`, `property`, k, `err`, err) continue } ch <- prop.desc } } func (c *poolCollector) update(ch chan<- metric, pools []string, excludes regexpCollection) error { var wg sync.WaitGroup errChan := make(chan error, len(pools)) for _, pool := range pools { wg.Add(1) go func(pool string) { if err := c.updatePoolMetrics(ch, pool); err != nil { errChan <- err } wg.Done() }(pool) } wg.Wait() select { case err := <-errChan: return err default: return nil } } func (c *poolCollector) updatePoolMetrics(ch chan<- metric, pool string) error { p := c.client.Pool(pool) props, err := p.Properties(c.props...) if err != nil { return err } labelValues := []string{pool} for k, v := range props.Properties() { prop, err := poolProperties.find(k) if err != nil { c.log.Warn(propertyUnsupportedMsg, `help`, helpIssue, `collector`, `pool`, `property`, k, `err`, err) } if err = prop.push(ch, v, labelValues...); err != nil { return err } } return nil } func newPoolCollector(l *slog.Logger, c zfs.Client, props []string) (Collector, error) { return &poolCollector{log: l, client: c, props: props}, nil } ================================================ FILE: collector/pool_test.go ================================================ package collector import ( "context" "strings" "testing" "github.com/pdf/zfs_exporter/v2/zfs/mock_zfs" "go.uber.org/mock/gomock" ) func TestPoolMetrics(t *testing.T) { testCases := []struct { name string pools []string explicitPools []string propsRequested []string metricNames []string propsResults map[string]map[string]string metricResults string }{ { name: `all metrics`, pools: []string{`testpool`}, propsRequested: []string{`allocated`, `dedupratio`, `capacity`, `expandsize`, `fragmentation`, `free`, `freeing`, `health`, `leaked`, `readonly`, `size`}, metricNames: []string{`zfs_pool_allocated_bytes`, `zfs_pool_deduplication_ratio`, `zfs_pool_capacity_ratio`, `zfs_pool_expand_size_bytes`, `zfs_pool_fragmentation_ratio`, `zfs_pool_free_bytes`, `zfs_pool_freeing_bytes`, `zfs_pool_health`, `zfs_pool_leaked_bytes`, `zfs_pool_readonly`, `zfs_pool_size_bytes`}, propsResults: map[string]map[string]string{ `testpool`: { `allocated`: `1024`, `dedupratio`: `2.50`, `capacity`: `50`, `expandsize`: `2048`, `fragmentation`: `25`, `free`: `1024`, `freeing`: `0`, `health`: `ONLINE`, `leaked`: `1`, `readonly`: `off`, `size`: `2048`, }, }, metricResults: `# HELP zfs_pool_allocated_bytes Amount of storage in bytes used within the pool. # TYPE zfs_pool_allocated_bytes gauge zfs_pool_allocated_bytes{pool="testpool"} 1024 # HELP zfs_pool_capacity_ratio Ratio of pool space used. # TYPE zfs_pool_capacity_ratio gauge zfs_pool_capacity_ratio{pool="testpool"} 0.5 # HELP zfs_pool_deduplication_ratio The ratio of deduplicated size vs undeduplicated size for data in this pool. # TYPE zfs_pool_deduplication_ratio gauge zfs_pool_deduplication_ratio{pool="testpool"} 0.4 # HELP zfs_pool_expand_size_bytes Amount of uninitialized space within the pool or device that can be used to increase the total capacity of the pool. # TYPE zfs_pool_expand_size_bytes gauge zfs_pool_expand_size_bytes{pool="testpool"} 2048 # HELP zfs_pool_fragmentation_ratio The fragmentation ratio of the pool. # TYPE zfs_pool_fragmentation_ratio gauge zfs_pool_fragmentation_ratio{pool="testpool"} 0.25 # HELP zfs_pool_free_bytes The amount of free space in bytes available in the pool. # TYPE zfs_pool_free_bytes gauge zfs_pool_free_bytes{pool="testpool"} 1024 # HELP zfs_pool_freeing_bytes The amount of space in bytes remaining to be freed following the destruction of a file system or snapshot. # TYPE zfs_pool_freeing_bytes gauge zfs_pool_freeing_bytes{pool="testpool"} 0 # HELP zfs_pool_health Health status code for the pool [0: ONLINE, 1: DEGRADED, 2: FAULTED, 3: OFFLINE, 4: UNAVAIL, 5: REMOVED, 6: SUSPENDED]. # TYPE zfs_pool_health gauge zfs_pool_health{pool="testpool"} 0 # HELP zfs_pool_leaked_bytes Number of leaked bytes in the pool. # TYPE zfs_pool_leaked_bytes gauge zfs_pool_leaked_bytes{pool="testpool"} 1 # HELP zfs_pool_readonly Read-only status of the pool [0: read-write, 1: read-only]. # TYPE zfs_pool_readonly gauge zfs_pool_readonly{pool="testpool"} 0 # HELP zfs_pool_size_bytes Total size in bytes of the storage pool. # TYPE zfs_pool_size_bytes gauge zfs_pool_size_bytes{pool="testpool"} 2048 `, }, { name: `multiple pools`, pools: []string{`testpool1`, `testpool2`}, propsRequested: []string{`allocated`}, metricNames: []string{`zfs_pool_allocated_bytes`}, propsResults: map[string]map[string]string{ `testpool1`: { `allocated`: `1024`, }, `testpool2`: { `allocated`: `2048`, }, }, metricResults: `# HELP zfs_pool_allocated_bytes Amount of storage in bytes used within the pool. # TYPE zfs_pool_allocated_bytes gauge zfs_pool_allocated_bytes{pool="testpool1"} 1024 zfs_pool_allocated_bytes{pool="testpool2"} 2048 `, }, { name: `explicit pools`, pools: []string{`testpool1`, `testpool2`}, explicitPools: []string{`testpool1`}, propsRequested: []string{`allocated`}, metricNames: []string{`zfs_pool_allocated_bytes`}, propsResults: map[string]map[string]string{ `testpool1`: { `allocated`: `1024`, }, `testpool2`: { `allocated`: `2048`, }, }, metricResults: `# HELP zfs_pool_allocated_bytes Amount of storage in bytes used within the pool. # TYPE zfs_pool_allocated_bytes gauge zfs_pool_allocated_bytes{pool="testpool1"} 1024 `, }, { name: `health status`, pools: []string{`onlinepool`, `degradedpool`, `faultedpool`, `offlinepool`, `unavailpool`, `removedpool`, `suspendedpool`}, propsRequested: []string{`health`}, metricNames: []string{`zfs_pool_health`}, propsResults: map[string]map[string]string{ `onlinepool`: { `health`: `ONLINE`, }, `degradedpool`: { `health`: `DEGRADED`, }, `faultedpool`: { `health`: `FAULTED`, }, `offlinepool`: { `health`: `OFFLINE`, }, `unavailpool`: { `health`: `UNAVAIL`, }, `removedpool`: { `health`: `REMOVED`, }, `suspendedpool`: { `health`: `SUSPENDED`, }, }, metricResults: `# HELP zfs_pool_health Health status code for the pool [0: ONLINE, 1: DEGRADED, 2: FAULTED, 3: OFFLINE, 4: UNAVAIL, 5: REMOVED, 6: SUSPENDED]. # TYPE zfs_pool_health gauge zfs_pool_health{pool="onlinepool"} 0 zfs_pool_health{pool="degradedpool"} 1 zfs_pool_health{pool="faultedpool"} 2 zfs_pool_health{pool="offlinepool"} 3 zfs_pool_health{pool="unavailpool"} 4 zfs_pool_health{pool="removedpool"} 5 zfs_pool_health{pool="suspendedpool"} 6 `, }, { name: `unsupported metric`, pools: []string{`testpool`}, propsRequested: []string{`unsupported`}, metricNames: []string{`zfs_pool_unsupported`}, propsResults: map[string]map[string]string{ `testpool`: { `unsupported`: `1024`, }, }, metricResults: `# HELP zfs_pool_unsupported !!! This property is unsupported, results are likely to be undesirable, please file an issue at https://github.com/pdf/zfs_exporter/issues to have this property supported !!! # TYPE zfs_pool_unsupported gauge zfs_pool_unsupported{pool="testpool"} 1024 `, }, { name: `legacy fragmentation/dedupratio`, pools: []string{`testpool`}, propsRequested: []string{`fragmentation`, `dedupratio`}, metricNames: []string{`zfs_pool_fragmentation_ratio`, `zfs_pool_deduplication_ratio`}, propsResults: map[string]map[string]string{ `testpool`: { `fragmentation`: `5%`, `dedupratio`: `2.50x`, }, }, metricResults: `# HELP zfs_pool_fragmentation_ratio The fragmentation ratio of the pool. # TYPE zfs_pool_fragmentation_ratio gauge zfs_pool_fragmentation_ratio{pool="testpool"} 0.05 # HELP zfs_pool_deduplication_ratio The ratio of deduplicated size vs undeduplicated size for data in this pool. # TYPE zfs_pool_deduplication_ratio gauge zfs_pool_deduplication_ratio{pool="testpool"} 0.4 `, }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() ctrl, ctx := gomock.WithContext(context.Background(), t) zfsClient := mock_zfs.NewMockClient(ctrl) config := defaultConfig(zfsClient) if tc.explicitPools != nil { config.Pools = tc.explicitPools } zfsClient.EXPECT().PoolNames().Return(tc.pools, nil).Times(1) for _, pool := range tc.pools { if tc.explicitPools != nil { wanted := false for _, explicit := range tc.explicitPools { if pool == explicit { wanted = true } break } if !wanted { continue } } zfsPoolProperties := mock_zfs.NewMockPoolProperties(ctrl) zfsPoolProperties.EXPECT().Properties().Return(tc.propsResults[pool]).Times(1) zfsPool := mock_zfs.NewMockPool(ctrl) zfsPool.EXPECT().Properties(tc.propsRequested).Return(zfsPoolProperties, nil).Times(1) zfsClient.EXPECT().Pool(pool).Return(zfsPool).Times(1) } collector, err := NewZFS(config) if err != nil { t.Fatal(err) } collector.Collectors = map[string]State{ `pool`: { Name: "pool", Enabled: boolPointer(true), Properties: stringPointer(strings.Join(tc.propsRequested, `,`)), factory: newPoolCollector, }, } if err = callCollector(ctx, collector, []byte(tc.metricResults), tc.metricNames); err != nil { t.Fatal(err) } }) } } ================================================ FILE: collector/transform.go ================================================ package collector import ( "fmt" "strconv" "github.com/pdf/zfs_exporter/v2/zfs" ) type poolHealthCode int const ( poolOnline poolHealthCode = iota poolDegraded poolFaulted poolOffline poolUnavail poolRemoved poolSuspended ) func transformNumeric(value string) (float64, error) { if value == `-` || value == `none` { return 0, nil } return strconv.ParseFloat(value, 64) } func transformHealthCode(status string) (float64, error) { var result poolHealthCode switch zfs.PoolStatus(status) { case zfs.PoolOnline: result = poolOnline case zfs.PoolDegraded: result = poolDegraded case zfs.PoolFaulted: result = poolFaulted case zfs.PoolOffline: result = poolOffline case zfs.PoolUnavail: result = poolUnavail case zfs.PoolRemoved: result = poolRemoved case zfs.PoolSuspended: result = poolSuspended default: return -1, fmt.Errorf(`unknown pool heath status: %s`, status) } return float64(result), nil } func transformBool(value string) (float64, error) { switch value { case `on`, `yes`, `enabled`, `active`: return 1, nil case `off`, `no`, `disabled`, `inactive`, `-`: return 0, nil } return -1, fmt.Errorf(`could not convert '%s' to bool`, value) } func transformPercentage(value string) (float64, error) { if len(value) > 0 && value[len(value)-1] == '%' { value = value[:len(value)-1] } v, err := transformNumeric(value) if err != nil { return -1, err } return v / 100, nil } func transformMultiplier(value string) (float64, error) { if len(value) > 0 && value[len(value)-1] == 'x' { value = value[:len(value)-1] } v, err := transformNumeric(value) if err != nil { return -1, err } return 1 / v, nil } ================================================ FILE: collector/zfs.go ================================================ package collector import ( "context" "log/slog" "regexp" "sort" "strings" "sync" "time" "github.com/pdf/zfs_exporter/v2/zfs" "github.com/prometheus/client_golang/prometheus" ) type regexpCollection []*regexp.Regexp func (c regexpCollection) MatchString(input string) bool { for _, r := range c { if r.MatchString(input) { return true } } return false } // ZFSConfig configures a ZFS collector type ZFSConfig struct { DisableMetrics bool Deadline time.Duration Pools []string Excludes []string Logger *slog.Logger ZFSClient zfs.Client } // ZFS collector type ZFS struct { Pools []string Collectors map[string]State client zfs.Client disableMetrics bool deadline time.Duration cache *metricCache ready chan struct{} logger *slog.Logger excludes regexpCollection } // Describe implements the prometheus.Collector interface. func (c *ZFS) Describe(ch chan<- *prometheus.Desc) { if !c.disableMetrics { ch <- scrapeDurationDesc ch <- scrapeSuccessDesc } for _, state := range c.Collectors { if !*state.Enabled { continue } collector, err := state.factory(c.logger, c.client, strings.Split(*state.Properties, `,`)) if err != nil { continue } collector.describe(ch) } } // Collect implements the prometheus.Collector interface. func (c *ZFS) Collect(ch chan<- prometheus.Metric) { select { case <-c.ready: default: c.sendCached(ch, make(map[string]struct{})) return } ctx, cancel := context.WithTimeout(context.Background(), c.deadline) defer cancel() cache := newMetricCache() proxy := make(chan metric) // Synchronize on collector completion. wg := sync.WaitGroup{} wg.Add(len(c.Collectors)) // Synchonize after timeout event, ensuring no writers are still active when we return control. timeout := make(chan struct{}) finalized := make(chan struct{}) finalize := func() { select { case <-finalized: default: close(finalized) } } // Close the proxy channel upon collector completion. go func() { wg.Wait() close(proxy) }() // Cache metrics as they come in via the proxy channel, and ship them out if we've not exceeded the deadline. go func() { for metric := range proxy { cache.add(metric) select { case <-timeout: finalize() default: ch <- metric.prometheus } } // Signal completion and update full cache. c.cache.replace(cache) cancel() // Notify next collection that we're ready to collect again c.ready <- struct{}{} }() pools, poolErr := c.getPools(c.Pools) for name, state := range c.Collectors { if !*state.Enabled { wg.Done() continue } if poolErr != nil { c.publishCollectorMetrics(ctx, name, poolErr, 0, proxy) wg.Done() continue } collector, err := state.factory(c.logger, c.client, strings.Split(*state.Properties, `,`)) if err != nil { c.logger.Error("Error instantiating collector", "collector", name, "err", err) wg.Done() continue } go func(name string, collector Collector) { c.execute(ctx, name, collector, proxy, pools) wg.Done() }(name, collector) } // Wait for completion or timeout <-ctx.Done() err := ctx.Err() if err == context.Canceled { finalize() } else if err != nil { // Upon exceeding deadline, send cached data for any metrics that have not already been reported. close(timeout) // assert timeout for flow control in other goroutines c.cache.merge(cache) cacheIndex := cache.index() c.sendCached(ch, cacheIndex) } // Ensure there are no in-flight writes to the upstream channel <-finalized } // sendCached values that do not appear in the current cacheIndex. func (c *ZFS) sendCached(ch chan<- prometheus.Metric, cacheIndex map[string]struct{}) { c.cache.RLock() defer c.cache.RUnlock() for name, metric := range c.cache.cache { if _, ok := cacheIndex[name]; ok { continue } ch <- metric } } func (c *ZFS) getPools(pools []string) ([]string, error) { poolNames, err := c.client.PoolNames() if err != nil { return nil, err } // Return all pools if not explicitly configured. if len(pools) == 0 { return poolNames, nil } // Configured pools may not exist, so append available pools as they're found, rather than allocating up front. result := make([]string, 0) for _, want := range pools { found := false for _, avail := range poolNames { if want == avail { result = append(result, want) found = true break } } if !found { c.logger.Warn("Pool unavailable", "pool", want) } } return result, nil } func (c *ZFS) execute(ctx context.Context, name string, collector Collector, ch chan<- metric, pools []string) { begin := time.Now() err := collector.update(ch, pools, c.excludes) duration := time.Since(begin) c.publishCollectorMetrics(ctx, name, err, duration, ch) } func (c *ZFS) publishCollectorMetrics(ctx context.Context, name string, err error, duration time.Duration, ch chan<- metric) { var success float64 if err != nil { c.logger.Error("Executing collector", "status", "error", "collector", name, "durationSeconds", duration.Seconds(), "err", err) success = 0 } else { select { case <-ctx.Done(): err = ctx.Err() default: err = nil } if err != nil && err != context.Canceled { c.logger.Warn("Executing collector", "status", "delayed", "collector", name, "durationSeconds", duration.Seconds(), "err", ctx.Err()) success = 0 } else { c.logger.Debug("Executing collector", "status", "ok", "collector", name, "durationSeconds", duration.Seconds()) success = 1 } } if c.disableMetrics { return } ch <- metric{ name: scrapeDurationDescName, prometheus: prometheus.MustNewConstMetric(scrapeDurationDesc, prometheus.GaugeValue, duration.Seconds(), name), } ch <- metric{ name: scrapeSuccessDescName, prometheus: prometheus.MustNewConstMetric(scrapeSuccessDesc, prometheus.GaugeValue, success, name), } } // NewZFS instantiates a ZFS collector with the provided ZFSConfig func NewZFS(config ZFSConfig) (*ZFS, error) { sort.Strings(config.Pools) sort.Strings(config.Excludes) excludes := make(regexpCollection, len(config.Excludes)) for i, v := range config.Excludes { excludes[i] = regexp.MustCompile(v) } ready := make(chan struct{}, 1) ready <- struct{}{} return &ZFS{ disableMetrics: config.DisableMetrics, client: config.ZFSClient, deadline: config.Deadline, Pools: config.Pools, Collectors: collectorStates, excludes: excludes, cache: newMetricCache(), ready: ready, logger: config.Logger, }, nil } ================================================ FILE: collector/zfs_test.go ================================================ package collector import ( "context" "errors" "testing" "github.com/pdf/zfs_exporter/v2/zfs/mock_zfs" "go.uber.org/mock/gomock" ) func TestZFSCollectInvalidPools(t *testing.T) { const result = `# HELP zfs_scrape_collector_duration_seconds zfs_exporter: Duration of a collector scrape. # TYPE zfs_scrape_collector_duration_seconds gauge zfs_scrape_collector_duration_seconds{collector="pool"} 0 # HELP zfs_scrape_collector_success zfs_exporter: Whether a collector succeeded. # TYPE zfs_scrape_collector_success gauge zfs_scrape_collector_success{collector="pool"} 0 ` ctrl, ctx := gomock.WithContext(context.Background(), t) zfsClient := mock_zfs.NewMockClient(ctrl) zfsClient.EXPECT().PoolNames().Return(nil, errors.New(`Error returned from PoolNames()`)).Times(1) config := defaultConfig(zfsClient) config.DisableMetrics = false collector, err := NewZFS(config) collector.Collectors = map[string]State{ `pool`: { Name: "pool", Enabled: boolPointer(true), Properties: stringPointer(``), factory: newPoolCollector, }, } if err != nil { t.Fatal(err) } if err = callCollector(ctx, collector, []byte(result), []string{`zfs_scrape_collector_duration_seconds`, `zfs_scrape_collector_success`}); err != nil { t.Fatal(err) } } ================================================ FILE: go.mod ================================================ module github.com/pdf/zfs_exporter/v2 go 1.24.0 toolchain go1.24.2 require ( github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/prometheus/client_golang v1.23.2 github.com/prometheus/common v0.67.4 golang.org/x/sys v0.38.0 // indirect ) require ( github.com/alecthomas/kingpin/v2 v2.4.0 github.com/prometheus/exporter-toolkit v0.15.0 go.uber.org/mock v0.6.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coreos/go-systemd/v22 v22.6.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/mdlayher/vsock v1.2.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/crypto v0.45.0 // indirect golang.org/x/mod v0.29.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.38.0 // indirect google.golang.org/protobuf v1.36.10 // indirect ) tool go.uber.org/mock/mockgen ================================================ FILE: go.sum ================================================ github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= 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/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/exporter-toolkit v0.15.0 h1:Pcle5sSViwR1x0gdPd0wtYrPQENBieQAM7TmT0qtb2U= github.com/prometheus/exporter-toolkit v0.15.0/go.mod h1:OyRWd2iTo6Xge9Kedvv0IhCrJSBu36JCfJ2yVniRIYk= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 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= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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/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= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: zfs/dataset.go ================================================ package zfs import ( "strings" ) // DatasetKind enum of supported dataset types type DatasetKind string const ( // DatasetFilesystem enum entry DatasetFilesystem DatasetKind = `filesystem` // DatasetVolume enum entry DatasetVolume DatasetKind = `volume` // DatasetSnapshot enum entry DatasetSnapshot DatasetKind = `snapshot` ) type datasetsImpl struct { pool string kind DatasetKind } func (d datasetsImpl) Pool() string { return d.pool } func (d datasetsImpl) Kind() DatasetKind { return d.kind } func (d datasetsImpl) Properties(props ...string) ([]DatasetProperties, error) { handler := newDatasetHandler() if err := execute(d.pool, handler, `zfs`, `get`, `-Hprt`, string(d.kind), `-o`, `name,property,value`, strings.Join(props, `,`)); err != nil { return nil, err } return handler.datasets(), nil } type datasetPropertiesImpl struct { datasetName string properties map[string]string } func (p *datasetPropertiesImpl) DatasetName() string { return p.datasetName } func (p *datasetPropertiesImpl) Properties() map[string]string { return p.properties } // datasetHandler handles parsing of the data returned from the CLI into Dataset structs type datasetHandler struct { store map[string]*datasetPropertiesImpl } // processLine implements the handler interface func (h *datasetHandler) processLine(pool string, line []string) error { if len(line) != 3 || !strings.HasPrefix(line[0], pool) { return ErrInvalidOutput } if _, ok := h.store[line[0]]; !ok { h.store[line[0]] = newDatasetPropertiesImpl(line[0]) } h.store[line[0]].properties[line[1]] = line[2] return nil } func (h *datasetHandler) datasets() []DatasetProperties { result := make([]DatasetProperties, len(h.store)) i := 0 for _, dataset := range h.store { result[i] = dataset i++ } return result } func newDatasetPropertiesImpl(name string) *datasetPropertiesImpl { return &datasetPropertiesImpl{ datasetName: name, properties: make(map[string]string), } } func newDatasetsImpl(pool string, kind DatasetKind) datasetsImpl { return datasetsImpl{ pool: pool, kind: kind, } } func newDatasetHandler() *datasetHandler { return &datasetHandler{ store: make(map[string]*datasetPropertiesImpl), } } ================================================ FILE: zfs/mock_zfs/mock_zfs.go ================================================ // Code generated by MockGen. DO NOT EDIT. // Source: zfs.go // // Generated by this command: // // mockgen -source=zfs.go -destination=mock_zfs/mock_zfs.go -package=mock_zfs // // Package mock_zfs is a generated GoMock package. package mock_zfs import ( reflect "reflect" zfs "github.com/pdf/zfs_exporter/v2/zfs" gomock "go.uber.org/mock/gomock" ) // MockClient is a mock of Client interface. type MockClient struct { ctrl *gomock.Controller recorder *MockClientMockRecorder isgomock struct{} } // MockClientMockRecorder is the mock recorder for MockClient. type MockClientMockRecorder struct { mock *MockClient } // NewMockClient creates a new mock instance. func NewMockClient(ctrl *gomock.Controller) *MockClient { mock := &MockClient{ctrl: ctrl} mock.recorder = &MockClientMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockClient) EXPECT() *MockClientMockRecorder { return m.recorder } // Datasets mocks base method. func (m *MockClient) Datasets(pool string, kind zfs.DatasetKind) zfs.Datasets { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Datasets", pool, kind) ret0, _ := ret[0].(zfs.Datasets) return ret0 } // Datasets indicates an expected call of Datasets. func (mr *MockClientMockRecorder) Datasets(pool, kind any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Datasets", reflect.TypeOf((*MockClient)(nil).Datasets), pool, kind) } // Pool mocks base method. func (m *MockClient) Pool(name string) zfs.Pool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Pool", name) ret0, _ := ret[0].(zfs.Pool) return ret0 } // Pool indicates an expected call of Pool. func (mr *MockClientMockRecorder) Pool(name any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pool", reflect.TypeOf((*MockClient)(nil).Pool), name) } // PoolNames mocks base method. func (m *MockClient) PoolNames() ([]string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PoolNames") ret0, _ := ret[0].([]string) ret1, _ := ret[1].(error) return ret0, ret1 } // PoolNames indicates an expected call of PoolNames. func (mr *MockClientMockRecorder) PoolNames() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PoolNames", reflect.TypeOf((*MockClient)(nil).PoolNames)) } // MockPool is a mock of Pool interface. type MockPool struct { ctrl *gomock.Controller recorder *MockPoolMockRecorder isgomock struct{} } // MockPoolMockRecorder is the mock recorder for MockPool. type MockPoolMockRecorder struct { mock *MockPool } // NewMockPool creates a new mock instance. func NewMockPool(ctrl *gomock.Controller) *MockPool { mock := &MockPool{ctrl: ctrl} mock.recorder = &MockPoolMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockPool) EXPECT() *MockPoolMockRecorder { return m.recorder } // Name mocks base method. func (m *MockPool) Name() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Name") ret0, _ := ret[0].(string) return ret0 } // Name indicates an expected call of Name. func (mr *MockPoolMockRecorder) Name() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockPool)(nil).Name)) } // Properties mocks base method. func (m *MockPool) Properties(props ...string) (zfs.PoolProperties, error) { m.ctrl.T.Helper() varargs := []any{} for _, a := range props { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "Properties", varargs...) ret0, _ := ret[0].(zfs.PoolProperties) ret1, _ := ret[1].(error) return ret0, ret1 } // Properties indicates an expected call of Properties. func (mr *MockPoolMockRecorder) Properties(props ...any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Properties", reflect.TypeOf((*MockPool)(nil).Properties), props...) } // MockPoolProperties is a mock of PoolProperties interface. type MockPoolProperties struct { ctrl *gomock.Controller recorder *MockPoolPropertiesMockRecorder isgomock struct{} } // MockPoolPropertiesMockRecorder is the mock recorder for MockPoolProperties. type MockPoolPropertiesMockRecorder struct { mock *MockPoolProperties } // NewMockPoolProperties creates a new mock instance. func NewMockPoolProperties(ctrl *gomock.Controller) *MockPoolProperties { mock := &MockPoolProperties{ctrl: ctrl} mock.recorder = &MockPoolPropertiesMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockPoolProperties) EXPECT() *MockPoolPropertiesMockRecorder { return m.recorder } // Properties mocks base method. func (m *MockPoolProperties) Properties() map[string]string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Properties") ret0, _ := ret[0].(map[string]string) return ret0 } // Properties indicates an expected call of Properties. func (mr *MockPoolPropertiesMockRecorder) Properties() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Properties", reflect.TypeOf((*MockPoolProperties)(nil).Properties)) } // MockDatasets is a mock of Datasets interface. type MockDatasets struct { ctrl *gomock.Controller recorder *MockDatasetsMockRecorder isgomock struct{} } // MockDatasetsMockRecorder is the mock recorder for MockDatasets. type MockDatasetsMockRecorder struct { mock *MockDatasets } // NewMockDatasets creates a new mock instance. func NewMockDatasets(ctrl *gomock.Controller) *MockDatasets { mock := &MockDatasets{ctrl: ctrl} mock.recorder = &MockDatasetsMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockDatasets) EXPECT() *MockDatasetsMockRecorder { return m.recorder } // Kind mocks base method. func (m *MockDatasets) Kind() zfs.DatasetKind { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Kind") ret0, _ := ret[0].(zfs.DatasetKind) return ret0 } // Kind indicates an expected call of Kind. func (mr *MockDatasetsMockRecorder) Kind() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Kind", reflect.TypeOf((*MockDatasets)(nil).Kind)) } // Pool mocks base method. func (m *MockDatasets) Pool() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Pool") ret0, _ := ret[0].(string) return ret0 } // Pool indicates an expected call of Pool. func (mr *MockDatasetsMockRecorder) Pool() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pool", reflect.TypeOf((*MockDatasets)(nil).Pool)) } // Properties mocks base method. func (m *MockDatasets) Properties(props ...string) ([]zfs.DatasetProperties, error) { m.ctrl.T.Helper() varargs := []any{} for _, a := range props { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "Properties", varargs...) ret0, _ := ret[0].([]zfs.DatasetProperties) ret1, _ := ret[1].(error) return ret0, ret1 } // Properties indicates an expected call of Properties. func (mr *MockDatasetsMockRecorder) Properties(props ...any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Properties", reflect.TypeOf((*MockDatasets)(nil).Properties), props...) } // MockDatasetProperties is a mock of DatasetProperties interface. type MockDatasetProperties struct { ctrl *gomock.Controller recorder *MockDatasetPropertiesMockRecorder isgomock struct{} } // MockDatasetPropertiesMockRecorder is the mock recorder for MockDatasetProperties. type MockDatasetPropertiesMockRecorder struct { mock *MockDatasetProperties } // NewMockDatasetProperties creates a new mock instance. func NewMockDatasetProperties(ctrl *gomock.Controller) *MockDatasetProperties { mock := &MockDatasetProperties{ctrl: ctrl} mock.recorder = &MockDatasetPropertiesMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockDatasetProperties) EXPECT() *MockDatasetPropertiesMockRecorder { return m.recorder } // DatasetName mocks base method. func (m *MockDatasetProperties) DatasetName() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DatasetName") ret0, _ := ret[0].(string) return ret0 } // DatasetName indicates an expected call of DatasetName. func (mr *MockDatasetPropertiesMockRecorder) DatasetName() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DatasetName", reflect.TypeOf((*MockDatasetProperties)(nil).DatasetName)) } // Properties mocks base method. func (m *MockDatasetProperties) Properties() map[string]string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Properties") ret0, _ := ret[0].(map[string]string) return ret0 } // Properties indicates an expected call of Properties. func (mr *MockDatasetPropertiesMockRecorder) Properties() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Properties", reflect.TypeOf((*MockDatasetProperties)(nil).Properties)) } // Mockhandler is a mock of handler interface. type Mockhandler struct { ctrl *gomock.Controller recorder *MockhandlerMockRecorder isgomock struct{} } // MockhandlerMockRecorder is the mock recorder for Mockhandler. type MockhandlerMockRecorder struct { mock *Mockhandler } // NewMockhandler creates a new mock instance. func NewMockhandler(ctrl *gomock.Controller) *Mockhandler { mock := &Mockhandler{ctrl: ctrl} mock.recorder = &MockhandlerMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *Mockhandler) EXPECT() *MockhandlerMockRecorder { return m.recorder } // processLine mocks base method. func (m *Mockhandler) processLine(pool string, line []string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "processLine", pool, line) ret0, _ := ret[0].(error) return ret0 } // processLine indicates an expected call of processLine. func (mr *MockhandlerMockRecorder) processLine(pool, line any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "processLine", reflect.TypeOf((*Mockhandler)(nil).processLine), pool, line) } ================================================ FILE: zfs/pool.go ================================================ package zfs import ( "bufio" "fmt" "io" "os/exec" "strings" ) // PoolStatus enum contains status text type PoolStatus string const ( // PoolOnline enum entry PoolOnline PoolStatus = `ONLINE` // PoolDegraded enum entry PoolDegraded PoolStatus = `DEGRADED` // PoolFaulted enum entry PoolFaulted PoolStatus = `FAULTED` // PoolOffline enum entry PoolOffline PoolStatus = `OFFLINE` // PoolUnavail enum entry PoolUnavail PoolStatus = `UNAVAIL` // PoolRemoved enum entry PoolRemoved PoolStatus = `REMOVED` // PoolSuspended enum entry PoolSuspended PoolStatus = `SUSPENDED` ) type poolImpl struct { name string } func (p poolImpl) Name() string { return p.name } func (p poolImpl) Properties(props ...string) (PoolProperties, error) { handler := newPoolPropertiesImpl() if err := execute(p.name, handler, `zpool`, `get`, `-Hpo`, `name,property,value`, strings.Join(props, `,`)); err != nil { return handler, err } return handler, nil } type poolPropertiesImpl struct { properties map[string]string } func (p *poolPropertiesImpl) Properties() map[string]string { return p.properties } // processLine implements the handler interface func (p *poolPropertiesImpl) processLine(pool string, line []string) error { if len(line) != 3 || line[0] != pool { return ErrInvalidOutput } p.properties[line[1]] = line[2] return nil } // PoolNames returns a list of available pool names func poolNames() ([]string, error) { pools := make([]string, 0) cmd := exec.Command(`zpool`, `list`, `-Ho`, `name`) out, err := cmd.StdoutPipe() if err != nil { return nil, err } stderr, err := cmd.StderrPipe() if err != nil { return nil, err } scanner := bufio.NewScanner(out) if err = cmd.Start(); err != nil { return nil, fmt.Errorf("failed to start command '%s': %w", cmd.String(), err) } for scanner.Scan() { pools = append(pools, scanner.Text()) } stde, _ := io.ReadAll(stderr) if err = cmd.Wait(); err != nil { return nil, fmt.Errorf("failed to execute command '%s'; output: '%s' (%w)", cmd.String(), strings.TrimSpace(string(stde)), err) } return pools, nil } func newPoolImpl(name string) poolImpl { return poolImpl{ name: name, } } func newPoolPropertiesImpl() *poolPropertiesImpl { return &poolPropertiesImpl{ properties: make(map[string]string), } } ================================================ FILE: zfs/zfs.go ================================================ //go:generate go tool go.uber.org/mock/mockgen -source=zfs.go -destination=mock_zfs/mock_zfs.go -package=mock_zfs package zfs import ( "encoding/csv" "errors" "fmt" "io" "os/exec" "strings" ) // ErrInvalidOutput is returned on unparseable CLI output var ErrInvalidOutput = errors.New(`invalid output executing command`) // Client is the primary entrypoint type Client interface { PoolNames() ([]string, error) Pool(name string) Pool Datasets(pool string, kind DatasetKind) Datasets } // Pool allows querying pool properties type Pool interface { Name() string Properties(props ...string) (PoolProperties, error) } // PoolProperties provides access to the properties for a pool type PoolProperties interface { Properties() map[string]string } // Datasets allows querying properties for datasets in a pool type Datasets interface { Pool() string Kind() DatasetKind Properties(props ...string) ([]DatasetProperties, error) } // DatasetProperties provides access to the properties for a dataset type DatasetProperties interface { DatasetName() string Properties() map[string]string } type handler interface { processLine(pool string, line []string) error } type clientImpl struct{} func (z clientImpl) PoolNames() ([]string, error) { return poolNames() } func (z clientImpl) Pool(name string) Pool { return newPoolImpl(name) } func (z clientImpl) Datasets(pool string, kind DatasetKind) Datasets { return newDatasetsImpl(pool, kind) } func execute(pool string, h handler, cmd string, args ...string) error { c := exec.Command(cmd, append(args, pool)...) out, err := c.StdoutPipe() if err != nil { return err } stderr, err := c.StderrPipe() if err != nil { return err } r := csv.NewReader(out) r.Comma = '\t' r.LazyQuotes = true r.ReuseRecord = true r.FieldsPerRecord = 3 if err = c.Start(); err != nil { return fmt.Errorf("failed to start command '%s': %w", c.String(), err) } for { line, err := r.Read() if errors.Is(err, io.EOF) { break } if err != nil { return err } if err = h.processLine(pool, line); err != nil { return err } } stde, _ := io.ReadAll(stderr) if err = c.Wait(); err != nil { return fmt.Errorf("failed to execute command '%s'; output: '%s' (%w)", c.String(), strings.TrimSpace(string(stde)), err) } return nil } // New instantiates a ZFS Client func New() Client { return clientImpl{} } ================================================ FILE: zfs_exporter.go ================================================ package main import ( "net/http" "os" "strings" "github.com/pdf/zfs_exporter/v2/collector" "github.com/pdf/zfs_exporter/v2/zfs" "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/exporter-toolkit/web" "github.com/prometheus/exporter-toolkit/web/kingpinflag" "github.com/prometheus/common/promslog" "github.com/prometheus/common/promslog/flag" "github.com/prometheus/common/version" ) func main() { var ( metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String() metricsExporterDisabled = kingpin.Flag(`web.disable-exporter-metrics`, `Exclude metrics about the exporter itself (promhttp_*, process_*, go_*).`).Default(`false`).Bool() deadline = kingpin.Flag("deadline", "Maximum duration that a collection should run before returning cached data. Should be set to a value shorter than your scrape timeout duration. The current collection run will continue and update the cache when complete (default: 8s)").Default("8s").Duration() pools = kingpin.Flag("pool", "Name of the pool(s) to collect, repeat for multiple pools (default: all pools).").Strings() excludes = kingpin.Flag("exclude", "Exclude datasets/snapshots/volumes that match the provided regex (e.g. '^rpool/docker/'), may be specified multiple times.").Strings() toolkitFlags = kingpinflag.AddFlags(kingpin.CommandLine, ":9134") ) promslogConfig := &promslog.Config{} flag.AddFlags(kingpin.CommandLine, promslogConfig) kingpin.Version(version.Print("zfs_exporter")) kingpin.HelpFlag.Short('h') kingpin.Parse() logger := promslog.New(promslogConfig) logger.Info("Starting zfs_exporter", "version", version.Info()) logger.Info("Build context", "context", version.BuildContext()) c, err := collector.NewZFS(collector.ZFSConfig{ DisableMetrics: *metricsExporterDisabled, Deadline: *deadline, Pools: *pools, Excludes: *excludes, Logger: logger, ZFSClient: zfs.New(), }) if err != nil { logger.Error("Error creating an exporter", "err", err) os.Exit(1) } if *metricsExporterDisabled { r := prometheus.NewRegistry() prometheus.DefaultRegisterer = r prometheus.DefaultGatherer = r } prometheus.MustRegister(c) prometheus.MustRegister(versioncollector.NewCollector("zfs_exporter")) if len(c.Pools) > 0 { logger.Info("Enabling pools", "pools", strings.Join(c.Pools, ", ")) } else { logger.Info("Enabling pools", "pools", "(all)") } collectorNames := make([]string, 0, len(c.Collectors)) for n, c := range c.Collectors { if *c.Enabled { collectorNames = append(collectorNames, n) } } logger.Info("Enabling collectors", "collectors", strings.Join(collectorNames, ", ")) http.Handle(*metricsPath, promhttp.Handler()) if *metricsPath != "/" { landingConfig := web.LandingConfig{ Name: "ZFS Exporter", Description: "Prometheus ZFS Exporter", Version: version.Info(), Links: []web.LandingLinks{ { Address: *metricsPath, Text: "Metrics", }, }, } landingPage, err := web.NewLandingPage(landingConfig) if err != nil { logger.Error("Error creating landing page", "err", err) os.Exit(1) } http.Handle("/", landingPage) } server := &http.Server{} err = web.ListenAndServe(server, toolkitFlags, logger) if err != nil { logger.Error("Error starting HTTP server", "err", err) os.Exit(1) } }