├── .github ├── osv-scanner-scheduled.yml └── workflows │ ├── make-release.yml │ └── run-tests.yml ├── .gitignore ├── .promu.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── VERSION ├── collector ├── collector_utils.go ├── fixtures │ ├── rest_config_devices_response.json │ ├── rest_db_status_response.json │ ├── rest_stats_device_response.json │ ├── rest_stats_device_response_missed_lastConnectionDurationS.json │ ├── rest_svc_report_response.json │ └── rest_system_connections_response.json ├── rest_config_devices.go ├── rest_config_devices_test.go ├── rest_db_status.go ├── rest_db_status_response.go ├── rest_db_status_test.go ├── rest_stats_device.go ├── rest_stats_device_test.go ├── rest_svc_report.go ├── rest_svc_report_test.go ├── rest_system_connections.go ├── rest_system_connections_response.go └── rest_system_connections_test.go ├── examples ├── exposed_parameters.md └── grafana │ ├── dashboard.json │ └── screenshot-1.png ├── go.mod ├── go.sum └── main.go /.github/osv-scanner-scheduled.yml: -------------------------------------------------------------------------------- 1 | name: OSV-Scanner Scheduled Scan 2 | 3 | on: 4 | schedule: 5 | - cron: "30 5 * * 1" 6 | # Change "main" to your default branch if you use a different name, i.e. "master" 7 | push: 8 | branches: [main] 9 | 10 | permissions: 11 | # Required to upload SARIF file to CodeQL. See: https://github.com/github/codeql-action/issues/2117 12 | actions: read 13 | # Require writing security events to upload SARIF file to security tab 14 | security-events: write 15 | # Only need to read contents 16 | contents: read 17 | 18 | jobs: 19 | scan-scheduled: 20 | uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v1.9.2" 21 | -------------------------------------------------------------------------------- /.github/workflows/make-release.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Make release 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | make_release_draft: 8 | description: Create release draft 9 | default: true 10 | type: boolean 11 | required: true 12 | build_dockerimage: 13 | description: Build docker images and push to docker hub 14 | default: true 15 | type: boolean 16 | required: true 17 | push: 18 | tags: 19 | - 'v*' 20 | 21 | 22 | jobs: 23 | build_release: 24 | name: Build release 25 | runs-on: ubuntu-22.04 26 | env: 27 | GOPATH: /home/runner/go 28 | steps: 29 | 30 | - name: Checkout code 31 | uses: actions/checkout@v3 32 | 33 | - name: Make crossbuild 34 | run: make crossbuild 35 | 36 | - name: Make tarballs 37 | run: make tarballs 38 | 39 | - name: Upload files to release and create draft 40 | # if: ${{ inputs.make_release_draft }} 41 | run: make release 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | - name: Cache artifacts 46 | uses: actions/cache@v3 47 | with: 48 | path: .build 49 | key: syncthing_exporter-${{ github.sha }} 50 | 51 | build_dockerimage: 52 | # if: ${{ inputs.build_dockerimage }} 53 | needs: build_release 54 | name: Build docker images 55 | runs-on: ubuntu-22.04 56 | steps: 57 | 58 | - name: Checkout code 59 | uses: actions/checkout@v3 60 | 61 | - name: Set VERSION as environment variable 62 | run: echo "VERSION=$(cat VERSION)" >> $GITHUB_ENV 63 | 64 | - name: Cache artifacts 65 | uses: actions/cache@v3 66 | with: 67 | path: .build 68 | key: syncthing_exporter-${{ github.sha }} 69 | 70 | - name: Set up QEMU 71 | uses: docker/setup-qemu-action@v2 72 | 73 | - name: Set up Docker Buildx 74 | uses: docker/setup-buildx-action@v2 75 | 76 | - name: Login to DockerHub 77 | uses: docker/login-action@v2 78 | with: 79 | username: ${{ secrets.DOCKERHUB_USERNAME }} 80 | password: ${{ secrets.DOCKERHUB_TOKEN }} 81 | 82 | - name: Build and push 83 | uses: docker/build-push-action@v4 84 | with: 85 | context: . 86 | platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v6,linux/arm/v7 87 | push: true 88 | target: ghactions 89 | tags: | 90 | f100024/syncthing_exporter:latest, 91 | f100024/syncthing_exporter:${{ env.VERSION }} 92 | env: 93 | DOCKER_BUILDKIT: 1 94 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Run tests 3 | on: 4 | pull_request: 5 | branches: 6 | - 'main' 7 | 8 | jobs: 9 | tests_with_cover: 10 | name: Run tests 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | - name: Install Go 16 | uses: actions/setup-go@v4 17 | with: 18 | go-version-file: 'go.mod' 19 | - name: Test 20 | run: go test -v -cover ./collector/ 21 | 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | .vscode/ 17 | .build/ 18 | .tarballs/ 19 | target/ 20 | 21 | # App binary 22 | syncthing_exporter -------------------------------------------------------------------------------- /.promu.yml: -------------------------------------------------------------------------------- 1 | go: 2 | version: 1.24 3 | cgo: false 4 | repository: 5 | path: github.com/f100024/syncthing_exporter 6 | build: 7 | prefix: out/ 8 | binaries: 9 | - name: syncthing_exporter 10 | flags: -a -tags netgo 11 | ldflags: | 12 | -s 13 | -X github.com/prometheus/common/version.Version={{.Version}} 14 | -X github.com/prometheus/common/version.Revision={{.Revision}} 15 | -X github.com/prometheus/common/version.Branch={{.Branch}} 16 | -X github.com/prometheus/common/version.BuildUser=local 17 | -X 'github.com/prometheus/common/version.BuildDate={{date "02 Jan 2006 15:04:05 MST"}}' 18 | tarball: 19 | prefix: . 20 | files: 21 | - syncthing_exporter 22 | - LICENSE 23 | - NOTICE 24 | crossbuild: 25 | platforms: 26 | - darwin/amd64 27 | - darwin/arm64 28 | - dragonfly/amd64 29 | - freebsd/386 30 | - freebsd/amd64 31 | - freebsd/arm64 32 | - freebsd/armv6 33 | - freebsd/armv7 34 | - illumos/amd64 35 | - linux/386 36 | - linux/amd64 37 | - linux/arm64 38 | - linux/armv5 39 | - linux/armv6 40 | - linux/armv7 41 | - linux/mips 42 | - linux/mips64 43 | - linux/mips64le 44 | - linux/mipsle 45 | - linux/ppc64 46 | - linux/ppc64le 47 | - linux/riscv64 48 | - linux/s390x 49 | - netbsd/amd64 50 | - netbsd/arm64 51 | - netbsd/armv6 52 | - netbsd/armv7 53 | - openbsd/386 54 | - openbsd/amd64 55 | - openbsd/arm64 56 | - openbsd/armv7 57 | - windows/386 58 | - windows/amd64 59 | - windows/arm64 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.12 / 2025-04-14 2 | --- 3 | * Fix "Docker instances crashing, seemingly at random" (#30) 4 | * Minor fixes 5 | ## 0.3.11 / 2025-04-11 6 | --- 7 | * Added support /rest/config/devices (#29) 8 | * Updated dependencies 9 | * Minor fixes 10 | ## 0.3.10 / 2025-02-24 11 | --- 12 | * Switched from promlog to promslog 13 | * Switched to go 1.24 14 | * Updated dependencies 15 | * Minor fixes 16 | ## 0.3.9 / 2024-12-23 17 | --- 18 | * Updated dependencies 19 | * Minor fixes 20 | ## 0.3.8 / 2024-08-26 21 | --- 22 | * Switched to go1.23 23 | * Updated dependencies 24 | ## 0.3.7 / 2023-03-20 25 | --- 26 | * Switched to go1.22 27 | * Updated dependencies 28 | 29 | ## 0.3.6 / 2023-10-27 30 | --- 31 | * Fixed bug (#25) node reporting down 32 | * Updated Grafana Dashboard example 33 | * Added linux/riscv64 build 34 | * Switched to go1.21 35 | * Updated dependencies 36 | ## 0.3.5 / 2023-06-08 37 | --- 38 | * Switched to go1.20 39 | * Updated dependencies 40 | ## 0.3.4 / 2022-12-02 41 | --- 42 | * Fixed crash due to missing value `lastConnectionDurationS` (thanks to @benediktschlager) 43 | * Fixed arm* docker image (thanks to @Luxtech) 44 | * Switched to go1.19 45 | * Updated dependencies 46 | ## 0.3.3 / 2022-06-12 47 | --- 48 | * Switch to go1.18 49 | * Updated dependencies 50 | * Updated promu to 0.13.0 51 | 52 | ## 0.3.2 / 2021-10-19 53 | --- 54 | * Switch to go1.17 55 | * Updated dependencies for compatibility with go1.17 56 | * Updated promu to 0.12.0 57 | * Added multiple platforms 58 | 59 | ## 0.3.1 / 2021-04-15 60 | --- 61 | * Created new metric `last_connection_timestamp` 62 | * Removed the `lastSeen` label from `last_connection_duration` 63 | 64 | ## 0.3.0 / 2021-04-15 65 | --- 66 | * Added support /rest/db/status 67 | * Fixed logging 68 | * Updated grafana dashboard 69 | * Refactoring 70 | 71 | ## 0.2.4 / 2021-04-13 72 | --- 73 | * Added support /rest/stats/device 74 | * Minor refactoring 75 | 76 | ## 0.2.3 / 2021-03-31 77 | --- 78 | * Added information about using exporter with docker 79 | 80 | ## 0.2.2 / 2021-03-31 81 | --- 82 | * Swithched to go 1.16 83 | * Update some dependencies 84 | 85 | ## 0.2.1 / 2020-12-30 86 | --- 87 | * Added CI Github actions 88 | * Environment variables renamed and clarified 89 | * Added required flag to start parameters 90 | 91 | ## 0.1.0 / 2020-10-14 92 | --- 93 | * Initial release 94 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 as builder 2 | COPY . /builddir 3 | WORKDIR /builddir 4 | 5 | RUN make build 6 | 7 | FROM alpine:3.21 as local 8 | COPY --from=builder /builddir/syncthing_exporter /usr/bin/syncthing_exporter 9 | 10 | EXPOSE 9093 11 | ENTRYPOINT ["syncthing_exporter"] 12 | 13 | FROM alpine:3.21 as ghactions 14 | ARG TARGETOS TARGETARCH TARGETVARIANT 15 | COPY .build/${TARGETOS}-${TARGETARCH}${TARGETVARIANT}/syncthing_exporter /usr/bin/syncthing_exporter 16 | 17 | EXPOSE 9093 18 | ENTRYPOINT ["syncthing_exporter"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Artem D. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | GO := go 3 | PROMU := $(GOPATH)/bin/promu 4 | PROMU_VERSION := 0.15.0 5 | PKGS = $(shell $(GO) list ./... | grep -v /vendor/) 6 | PREFIX ?= $(shell pwd) 7 | BIN_DIR ?= $(shell pwd) 8 | 9 | 10 | all: test build 11 | 12 | test: 13 | @echo ">> running tests" 14 | go test -short $(PKGS) 15 | 16 | build: promu 17 | @echo ">> building binaries" 18 | @$(PROMU) build --prefix $(PREFIX) 19 | 20 | tarball: promu 21 | @echo ">> building release tarball" 22 | @$(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) 23 | 24 | tarballs: promu 25 | @echo ">> building release tarballs" 26 | @$(PROMU) crossbuild tarballs 27 | @echo ">> calculating release checksums" 28 | @$(PROMU) checksum $(BIN_DIR)/.tarballs 29 | 30 | crossbuild: promu 31 | @echo ">> cross-building binaries" 32 | @$(PROMU) crossbuild 33 | 34 | release: promu 35 | @echo ">> create release" 36 | @$(PROMU) release --verbose --timeout=120s --retry=30 $(BIN_DIR)/.tarballs 37 | 38 | promu: 39 | @GOOS=$(shell uname -s | tr A-Z a-z) \ 40 | GOARCH=$(subst x86_64,amd64,$(patsubst i%86,386,$(shell uname -m))) \ 41 | $(GO) install github.com/prometheus/promu@v$(PROMU_VERSION) 42 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Syncthing exporter 2 | Copyright 2020 Artem D. 3 | 4 | 5 | The following components are included in this product: 6 | 7 | Syncthing 8 | https://github.com/syncthing/syncthing 9 | Licensed under the Mozilla Public License Version 2.0 10 | 11 | Inspired by: 12 | Elasticsearch Exporter 13 | https://github.com/justwatchcom/elasticsearch_exporter 14 | Licensed under the Apache License 2.0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Syncthing exporter 2 | 3 | ### Build and run exporter 4 | 5 | Clone current repository and 6 | ```bash 7 | 8 | cd syncthing_exporter 9 | go build . 10 | 11 | ./syncthing_exporter --version 12 | ``` 13 | 14 | For pre-built binaries please take a look at the releases. 15 | 16 | Basic prometheus configuration: 17 | 18 | ```yaml 19 | - job_name: 'syncthing_server' 20 | metrics_path: /metrics 21 | static_configs: 22 | - targets: ['127.0.0.1:9093'] 23 | labels: 24 | service: syncthing_server 25 | ``` 26 | 27 | ### Start flags 28 | 29 | Name | Evironment variable | Required | Description 30 | --------------------|---------------------|----------|------------- 31 | web.listen-address | WEB_LISTEN_ADDRESS | - | Address ot listen on for web interface and telemetry 32 | web.metrics-path | WEB_METRIC_PATH | - | Path under which to expose metrics 33 | syncthing.uri | SYNCTHING_URI | + | HTTP API address of Syncthing node 34 | syncthing.token | SYNCTHING_TOKEN | + | Token for authentification Syncthing HTTP API 35 | syncthing.timeout | SYNCTHING_TIMEOUT | - | Timeout for trying to get stats from Syncthing 36 | syncthing.foldersid | SYNCTHING_FOLDERSID | - | List of ids of folders, delimeter is ',' 37 | 38 | ### What's and how exported 39 | 40 | Example of all metrics related to `syncthing` [here](examples/exposed_parameters.md). 41 | 42 | For data obtaining is using five endpoints: 43 | 44 | [GET /rest/svc/report](https://docs.syncthing.net/rest/svc-report-get.html) 45 | [GET /rest/system/connections](https://docs.syncthing.net/rest/system-connections-get.html) 46 | [GET /rest/stats/device](https://docs.syncthing.net/rest/stats-device-get.html) 47 | [GET /rest/db/status](https://docs.syncthing.net/rest/db-status-get.html)* 48 | [GET /rest/config/devices](https://docs.syncthing.net/rest/config.html) 49 | 50 | >\* This is an expensive call, increasing CPU and RAM usage on the device. Use sparingly. 51 | 52 | ### Grafana dashboard 53 | 54 | Example of grafana dashboard: 55 | 56 |  57 | 58 | 59 | ## Docker support 60 | How to run 61 | ``` 62 | docker run -it -e "SYNCTHING_URI=http://127.0.0.1:8384/" -e SYNCTHING_TOKEN="super-secure-token" f100024/syncthing_exporter:latest 63 | ``` 64 | 65 | docker-compose example: 66 | ``` 67 | syncthing_exporter: 68 | image: f100024/syncthing_exporter:latest 69 | ports: 70 | - 9093:9093 71 | environment: 72 | SYNCTHING_URI: "http://127.0.0.1:8384/" 73 | SYNCTHING_TOKEN: "super-secure-token" 74 | restart: unless-stopped 75 | ``` 76 | 77 | 78 | ## Communal effort 79 | Any ideas and pull requests are appreciated. 80 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.3.12 2 | -------------------------------------------------------------------------------- /collector/collector_utils.go: -------------------------------------------------------------------------------- 1 | // Package collector is entry point of collector 2 | package collector 3 | 4 | import ( 5 | "crypto/tls" 6 | "net/http" 7 | ) 8 | 9 | const ( 10 | namespace = "syncthing" 11 | ) 12 | 13 | var ( 14 | // HTTPClient skip insecure connection verify due to using self signed certificate on syncthing side. 15 | HTTPClient = &http.Client{ 16 | Transport: &http.Transport{ 17 | TLSClientConfig: &tls.Config{ 18 | InsecureSkipVerify: true, 19 | }, 20 | }, 21 | } 22 | ) 23 | 24 | func bool2float64(status bool) float64 { 25 | if status { 26 | return float64(1) 27 | } 28 | return float64(0) 29 | } 30 | -------------------------------------------------------------------------------- /collector/fixtures/rest_config_devices_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "deviceID": "EVOOQ2B-NZSX5XA-MGCOKJ7-M53F43K-HBKQOAS-7S5POY5-DSCKCTH-AI4T2AJ", 4 | "name": "host-1", 5 | "addresses": [ 6 | "dynamic" 7 | ], 8 | "compression": "metadata", 9 | "certName": "", 10 | "introducer": false, 11 | "skipIntroductionRemovals": false, 12 | "introducedBy": "", 13 | "paused": false, 14 | "allowedNetworks": [], 15 | "autoAcceptFolders": false, 16 | "maxSendKbps": 0, 17 | "maxRecvKbps": 0, 18 | "ignoredFolders": [], 19 | "maxRequestKiB": 0, 20 | "untrusted": false, 21 | "remoteGUIPort": 0, 22 | "numConnections": 0 23 | }, 24 | { 25 | "deviceID": "KXPN5AW-5EAJEA3-F7GXDVS-HPTV57U-NWDMFCS-SCTDFQF-CGGQTQD-FV2HAAZ", 26 | "name": "host-2", 27 | "addresses": [ 28 | "tcp://192.168.1.2:22000" 29 | ], 30 | "compression": "metadata", 31 | "certName": "", 32 | "introducer": false, 33 | "skipIntroductionRemovals": false, 34 | "introducedBy": "", 35 | "paused": false, 36 | "allowedNetworks": [], 37 | "autoAcceptFolders": true, 38 | "maxSendKbps": 0, 39 | "maxRecvKbps": 0, 40 | "ignoredFolders": [], 41 | "maxRequestKiB": 0, 42 | "untrusted": false, 43 | "remoteGUIPort": 0, 44 | "numConnections": 0 45 | } 46 | ] -------------------------------------------------------------------------------- /collector/fixtures/rest_db_status_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": 0, 3 | "globalBytes": 4047955455069, 4 | "globalDeleted": 156112, 5 | "globalDirectories": 346372, 6 | "globalFiles": 694624, 7 | "globalSymlinks": 0, 8 | "globalTotalItems": 1197108, 9 | "ignorePatterns": false, 10 | "inSyncBytes": 4047955455069, 11 | "inSyncFiles": 694624, 12 | "invalid": "", 13 | "localBytes": 4047955455069, 14 | "localDeleted": 318, 15 | "localDirectories": 346372, 16 | "localFiles": 694624, 17 | "localSymlinks": 0, 18 | "localTotalItems": 1041314, 19 | "needBytes": 0, 20 | "needDeletes": 0, 21 | "needDirectories": 0, 22 | "needFiles": 0, 23 | "needSymlinks": 0, 24 | "needTotalItems": 0, 25 | "pullErrors": 0, 26 | "sequence": 1213411, 27 | "state": "idle", 28 | "stateChanged": "2077-02-06T00:02:00-07:00", 29 | "version": 1213411 30 | } -------------------------------------------------------------------------------- /collector/fixtures/rest_stats_device_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA": { 3 | "lastSeen": "2077-12-18T00:00:50.3810375-08:00", 4 | "lastConnectionDurationS": 0 5 | }, 6 | "AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB": { 7 | "lastSeen": "2077-04-13T04:50:25-07:00", 8 | "lastConnectionDurationS": 819990.3432191 9 | }, 10 | "AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC": { 11 | "lastSeen": "1984-12-31T16:00:00-08:00", 12 | "lastConnectionDurationS": 0 13 | } 14 | } -------------------------------------------------------------------------------- /collector/fixtures/rest_stats_device_response_missed_lastConnectionDurationS.json: -------------------------------------------------------------------------------- 1 | { 2 | "AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA": { 3 | "lastSeen": "2077-12-18T00:00:50.3810375-08:00", 4 | "lastConnectionDurationS": 0 5 | }, 6 | "AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB": { 7 | "lastSeen": "2077-04-13T04:50:25-07:00", 8 | "lastConnectionDurationS": 819990.3432191 9 | }, 10 | "AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC": { 11 | "lastSeen": "1984-12-31T16:00:00-08:00" 12 | } 13 | } -------------------------------------------------------------------------------- /collector/fixtures/rest_svc_report_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "uniqueID": "d9uZew76", 3 | "version": "v1.9.0", 4 | "longVersion": "syncthing v1.9.0 \"Fermium Flea\" (go1.15.1 windows-amd64) teamcity@build.syncthing.net 2020-08-28 05:48:25 UTC", 5 | "platform": "windows-amd64", 6 | "numFolders": 2, 7 | "numDevices": 3, 8 | "totFiles": 509466, 9 | "folderMaxFiles": 484086, 10 | "totMiB": 3509532, 11 | "folderMaxMiB": 3509430, 12 | "memoryUsageMiB": 219, 13 | "sha256Perf": 216.93, 14 | "hashPerf": 187.47, 15 | "memorySize": 8191, 16 | "urVersion": 3, 17 | "numCPU": 4, 18 | "folderUses": { 19 | "sendonly": 2, 20 | "autoNormalize": 2 21 | }, 22 | "deviceUses": { 23 | "compressMetadata": 3, 24 | "dynamicAddr": 1, 25 | "staticAddr": 2 26 | }, 27 | "announce": { 28 | "localEnabled": true, 29 | "defaultServersDNS": 1 30 | }, 31 | "relays": { 32 | "defaultServers": 1 33 | }, 34 | "upgradeAllowedManual": true, 35 | "upgradeAllowedAuto": true, 36 | "rescanIntvs": [ 37 | 3600, 38 | 3600 39 | ], 40 | "uptime": 1276967, 41 | "natType": "Port restricted NAT", 42 | "progressEmitterEnabled": true, 43 | "customReleaseURL": true, 44 | "restartOnWakeup": true, 45 | "folderUsesV3": { 46 | "conflictsOther": 2, 47 | "fsWatcherEnabled": 2, 48 | "pullOrder": { 49 | "random": 2 50 | }, 51 | "filesystemType": { 52 | "basic": 2 53 | }, 54 | "fsWatcherDelays": [ 55 | 10, 56 | 10 57 | ], 58 | "modTimeWindowS": [ 59 | 0, 60 | 0 61 | ], 62 | "maxConcurrentWrites": [ 63 | 0, 64 | 0 65 | ], 66 | "blockPullOrder": { 67 | "standard": 2 68 | }, 69 | "copyRangeMethod": { 70 | "standard": 2 71 | } 72 | }, 73 | "guiStats": { 74 | "enabled": 1, 75 | "useAuth": 1, 76 | "listenUnspecified": 1, 77 | "theme": { 78 | "dark": 1 79 | } 80 | }, 81 | "blockStats": {}, 82 | "transportStats": { 83 | "tcp4": 2 84 | }, 85 | "ignoreStats": {} 86 | } -------------------------------------------------------------------------------- /collector/fixtures/rest_system_connections_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "connections": { 3 | "AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA": { 4 | "address": "10.1.0.1:59172", 5 | "at": "2020-09-18T03:03:32.5024716-07:00", 6 | "clientVersion": "v1.10.0", 7 | "connected": true, 8 | "crypto": "TLS1.3-TLS_AES_128_GCM_SHA256", 9 | "inBytesTotal": 1119463078, 10 | "outBytesTotal": 16360809260, 11 | "paused": false, 12 | "type": "tcp-server" 13 | }, 14 | "AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB": { 15 | "address": "10.2.0.2:22000", 16 | "at": "2020-09-18T03:03:32.5024716-07:00", 17 | "clientVersion": "v1.10.0", 18 | "connected": true, 19 | "crypto": "TLS1.3-TLS_CHACHA20_POLY1305_SHA256", 20 | "inBytesTotal": 10431439, 21 | "outBytesTotal": 6072477885, 22 | "paused": false, 23 | "type": "tcp-client" 24 | }, 25 | "AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC": { 26 | "address": "", 27 | "at": "0001-01-01T00:00:00Z", 28 | "clientVersion": "", 29 | "connected": false, 30 | "crypto": "", 31 | "inBytesTotal": 0, 32 | "outBytesTotal": 0, 33 | "paused": false, 34 | "type": "" 35 | } 36 | }, 37 | "total": { 38 | "address": "", 39 | "at": "2020-09-18T03:03:32.5024716-07:00", 40 | "clientVersion": "", 41 | "connected": false, 42 | "crypto": "", 43 | "inBytesTotal": 2458124935, 44 | "outBytesTotal": 33180203458, 45 | "paused": false, 46 | "type": "" 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /collector/rest_config_devices.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/prometheus/client_golang/prometheus" 12 | "github.com/syncthing/syncthing/lib/config" 13 | ) 14 | 15 | // CDResponseBoolMetrics defines struct for boolean metrics 16 | type CDResponseBoolMetrics struct { 17 | Type prometheus.ValueType 18 | Desc *prometheus.Desc 19 | Value func(v bool) float64 20 | } 21 | 22 | // ConfigDevicesResponse defines collector struct. 23 | type ConfigDevicesResponse struct { 24 | logger *slog.Logger 25 | client *http.Client 26 | url *url.URL 27 | token *string 28 | 29 | up prometheus.Gauge 30 | totalScrapes, jsonParseFailures prometheus.Counter 31 | boolMetrics map[string]*CDResponseBoolMetrics 32 | } 33 | 34 | // NewConfigDevicesReport returns a new Collector exposing ConfigDevicesResponse 35 | func NewConfigDevicesReport(logger *slog.Logger, client *http.Client, url *url.URL, token *string) *ConfigDevicesResponse { 36 | subsystem := "rest_config_devices" 37 | return &ConfigDevicesResponse{ 38 | logger: logger, 39 | client: client, 40 | url: url, 41 | token: token, 42 | up: prometheus.NewGauge(prometheus.GaugeOpts{ 43 | Name: prometheus.BuildFQName(namespace, subsystem, "up"), 44 | Help: "Was the last scrape of the Syncthing endpoint successful.", 45 | }), 46 | totalScrapes: prometheus.NewCounter(prometheus.CounterOpts{ 47 | Name: prometheus.BuildFQName(namespace, subsystem, "total_scrapes"), 48 | Help: "Current total Syncthings scrapes.", 49 | }), 50 | jsonParseFailures: prometheus.NewCounter(prometheus.CounterOpts{ 51 | Name: prometheus.BuildFQName(namespace, subsystem, "json_parse_failures"), 52 | Help: "Number of errors while parsing JSON.", 53 | }), 54 | boolMetrics: map[string]*CDResponseBoolMetrics{ 55 | "is_remote_device_paused": { 56 | Type: prometheus.GaugeValue, 57 | Desc: prometheus.NewDesc(prometheus.BuildFQName(namespace, subsystem, "is_remote_device_paused"), 58 | "Is remote device paused and other device information. 1 - paused, 0 - not paused", 59 | []string{"deviceID", "name", "addresses", "compression", "certName", "introducedBy", "allowedNetworks"}, nil), 60 | Value: func(v bool) float64 { 61 | return bool2float64(v) 62 | }, 63 | }, 64 | "is_remote_device_introducer": { 65 | Type: prometheus.GaugeValue, 66 | Desc: prometheus.NewDesc(prometheus.BuildFQName(namespace, subsystem, "is_remote_device_introducer"), 67 | "Is remote device marked as introducer. 1 - introducer, 0 - no.", 68 | []string{"deviceID", "name"}, nil), 69 | Value: func(v bool) float64 { 70 | return bool2float64(v) 71 | }, 72 | }, 73 | "is_remote_device_skip_introduction_removals": { 74 | Type: prometheus.GaugeValue, 75 | Desc: prometheus.NewDesc(prometheus.BuildFQName(namespace, subsystem, "is_remote_device_skip_introduction_removals"), 76 | "Is remote device skip introduction removals", 77 | []string{"deviceID", "name"}, nil), 78 | Value: func(v bool) float64 { 79 | return bool2float64(v) 80 | }, 81 | }, 82 | "is_remote_device_auto_accept_folders": { 83 | Type: prometheus.GaugeValue, 84 | Desc: prometheus.NewDesc(prometheus.BuildFQName(namespace, subsystem, "is_remote_device_auto_accept_folders"), 85 | "Is remote device auto accept folders", 86 | []string{"deviceID", "name"}, nil), 87 | Value: func(v bool) float64 { 88 | return bool2float64(v) 89 | }, 90 | }, 91 | "is_remote_device_untrusted": { 92 | Type: prometheus.GaugeValue, 93 | Desc: prometheus.NewDesc(prometheus.BuildFQName(namespace, subsystem, "is_remote_device_untrusted"), 94 | "Is remote device auto accept folders", 95 | []string{"deviceID", "name"}, nil), 96 | Value: func(v bool) float64 { 97 | return bool2float64(v) 98 | }, 99 | }, 100 | }, 101 | } 102 | } 103 | 104 | // Describe set Prometheus metrics descriptions. 105 | func (c *ConfigDevicesResponse) Describe(ch chan<- *prometheus.Desc) { 106 | for _, metric := range c.boolMetrics { 107 | ch <- metric.Desc 108 | } 109 | 110 | ch <- c.up.Desc() 111 | ch <- c.totalScrapes.Desc() 112 | ch <- c.jsonParseFailures.Desc() 113 | } 114 | 115 | func (c *ConfigDevicesResponse) fetchDataAndDecode() ([]config.DeviceConfiguration, error) { 116 | var chr []config.DeviceConfiguration 117 | 118 | u := *c.url 119 | url, _ := u.Parse("/rest/config/devices") 120 | 121 | h := make(http.Header) 122 | h["X-API-Key"] = []string{*c.token} 123 | 124 | request := &http.Request{ 125 | URL: url, 126 | Header: h, 127 | } 128 | 129 | res, err := c.client.Do(request) 130 | if err != nil { 131 | return chr, fmt.Errorf("failed to get data from %s://%s:%s%s: %s", 132 | u.Scheme, u.Hostname(), u.Port(), u.Path, err) 133 | } 134 | 135 | defer func() { 136 | err = res.Body.Close() 137 | if err != nil { 138 | c.logger.Warn(fmt.Sprintf("%s: %s", "failed to close http.Client", err)) 139 | } 140 | }() 141 | 142 | if res.StatusCode != http.StatusOK { 143 | return chr, fmt.Errorf("HTTP Request failed with code %d", res.StatusCode) 144 | } 145 | 146 | if err := json.NewDecoder(res.Body).Decode(&chr); err != nil { 147 | c.jsonParseFailures.Inc() 148 | return chr, err 149 | } 150 | 151 | return chr, nil 152 | } 153 | 154 | // Collect collects Syncthing metrics from /rest/config/devices. 155 | func (c *ConfigDevicesResponse) Collect(ch chan<- prometheus.Metric) { 156 | var err error 157 | c.totalScrapes.Inc() 158 | defer func() { 159 | ch <- c.up 160 | ch <- c.totalScrapes 161 | ch <- c.jsonParseFailures 162 | }() 163 | 164 | CDResponse, err := c.fetchDataAndDecode() 165 | 166 | if err != nil { 167 | c.up.Set(0) 168 | c.logger.Warn(fmt.Sprintf("%s: %s", "failed to fetch and decode data", err)) 169 | return 170 | } 171 | c.up.Set(1) 172 | 173 | for _, deviceProperty := range CDResponse { 174 | ch <- prometheus.MustNewConstMetric( 175 | c.boolMetrics["is_remote_device_paused"].Desc, 176 | c.boolMetrics["is_remote_device_paused"].Type, 177 | c.boolMetrics["is_remote_device_paused"].Value(deviceProperty.Paused), 178 | deviceProperty.DeviceID.String(), deviceProperty.Name, strings.Join(deviceProperty.Addresses, ","), 179 | deviceProperty.Compression.ToProtocol().Enum().String(), deviceProperty.CertName, deviceProperty.IntroducedBy.GoString(), 180 | strings.Join(deviceProperty.AllowedNetworks, ","), 181 | ) 182 | ch <- prometheus.MustNewConstMetric( 183 | c.boolMetrics["is_remote_device_introducer"].Desc, 184 | c.boolMetrics["is_remote_device_introducer"].Type, 185 | c.boolMetrics["is_remote_device_introducer"].Value(deviceProperty.Introducer), 186 | deviceProperty.DeviceID.String(), deviceProperty.Name, 187 | ) 188 | ch <- prometheus.MustNewConstMetric( 189 | c.boolMetrics["is_remote_device_skip_introduction_removals"].Desc, 190 | c.boolMetrics["is_remote_device_skip_introduction_removals"].Type, 191 | c.boolMetrics["is_remote_device_skip_introduction_removals"].Value(deviceProperty.SkipIntroductionRemovals), 192 | deviceProperty.DeviceID.String(), deviceProperty.Name, 193 | ) 194 | ch <- prometheus.MustNewConstMetric( 195 | c.boolMetrics["is_remote_device_auto_accept_folders"].Desc, 196 | c.boolMetrics["is_remote_device_auto_accept_folders"].Type, 197 | c.boolMetrics["is_remote_device_auto_accept_folders"].Value(deviceProperty.AutoAcceptFolders), 198 | deviceProperty.DeviceID.String(), deviceProperty.Name, 199 | ) 200 | ch <- prometheus.MustNewConstMetric( 201 | c.boolMetrics["is_remote_device_untrusted"].Desc, 202 | c.boolMetrics["is_remote_device_untrusted"].Type, 203 | c.boolMetrics["is_remote_device_untrusted"].Value(deviceProperty.Untrusted), 204 | deviceProperty.DeviceID.String(), deviceProperty.Name, 205 | ) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /collector/rest_config_devices_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "os" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/prometheus/client_golang/prometheus/testutil" 13 | "github.com/prometheus/common/promslog" 14 | ) 15 | 16 | func TestNewConfigDevicesReport(t *testing.T) { 17 | jsonResponse, _ := os.ReadFile("fixtures/rest_config_devices_response.json") 18 | 19 | ts := httptest.NewTLSServer( 20 | http.HandlerFunc( 21 | func(w http.ResponseWriter, _ *http.Request) { 22 | fmt.Fprintln(w, string(jsonResponse)) 23 | }, 24 | ), 25 | ) 26 | defer ts.Close() 27 | 28 | u, err := url.Parse(ts.URL) 29 | if err != nil { 30 | t.Errorf("url parse error: %s", err) 31 | } 32 | 33 | promslogConfig := &promslog.Config{} 34 | logger := promslog.New(promslogConfig) 35 | 36 | testToken := "12345" 37 | expected := ` 38 | # HELP syncthing_rest_config_devices_is_remote_device_auto_accept_folders Is remote device auto accept folders 39 | # TYPE syncthing_rest_config_devices_is_remote_device_auto_accept_folders gauge 40 | syncthing_rest_config_devices_is_remote_device_auto_accept_folders{deviceID="EVOOQ2B-NZSX5XA-MGCOKJ7-M53F43K-HBKQOAS-7S5POY5-DSCKCTH-AI4T2AJ",name="host-1"} 0 41 | syncthing_rest_config_devices_is_remote_device_auto_accept_folders{deviceID="KXPN5AW-5EAJEA3-F7GXDVS-HPTV57U-NWDMFCS-SCTDFQF-CGGQTQD-FV2HAAZ",name="host-2"} 1 42 | # HELP syncthing_rest_config_devices_is_remote_device_introducer Is remote device marked as introducer. 1 - introducer, 0 - no. 43 | # TYPE syncthing_rest_config_devices_is_remote_device_introducer gauge 44 | syncthing_rest_config_devices_is_remote_device_introducer{deviceID="EVOOQ2B-NZSX5XA-MGCOKJ7-M53F43K-HBKQOAS-7S5POY5-DSCKCTH-AI4T2AJ",name="host-1"} 0 45 | syncthing_rest_config_devices_is_remote_device_introducer{deviceID="KXPN5AW-5EAJEA3-F7GXDVS-HPTV57U-NWDMFCS-SCTDFQF-CGGQTQD-FV2HAAZ",name="host-2"} 0 46 | # HELP syncthing_rest_config_devices_is_remote_device_paused Is remote device paused and other device information. 1 - paused, 0 - not paused 47 | # TYPE syncthing_rest_config_devices_is_remote_device_paused gauge 48 | syncthing_rest_config_devices_is_remote_device_paused{addresses="dynamic",allowedNetworks="",certName="",compression="COMPRESSION_METADATA",deviceID="EVOOQ2B-NZSX5XA-MGCOKJ7-M53F43K-HBKQOAS-7S5POY5-DSCKCTH-AI4T2AJ",introducedBy="",name="host-1"} 0 49 | syncthing_rest_config_devices_is_remote_device_paused{addresses="tcp://192.168.1.2:22000",allowedNetworks="",certName="",compression="COMPRESSION_METADATA",deviceID="KXPN5AW-5EAJEA3-F7GXDVS-HPTV57U-NWDMFCS-SCTDFQF-CGGQTQD-FV2HAAZ",introducedBy="",name="host-2"} 0 50 | # HELP syncthing_rest_config_devices_is_remote_device_skip_introduction_removals Is remote device skip introduction removals 51 | # TYPE syncthing_rest_config_devices_is_remote_device_skip_introduction_removals gauge 52 | syncthing_rest_config_devices_is_remote_device_skip_introduction_removals{deviceID="EVOOQ2B-NZSX5XA-MGCOKJ7-M53F43K-HBKQOAS-7S5POY5-DSCKCTH-AI4T2AJ",name="host-1"} 0 53 | syncthing_rest_config_devices_is_remote_device_skip_introduction_removals{deviceID="KXPN5AW-5EAJEA3-F7GXDVS-HPTV57U-NWDMFCS-SCTDFQF-CGGQTQD-FV2HAAZ",name="host-2"} 0 54 | # HELP syncthing_rest_config_devices_is_remote_device_untrusted Is remote device auto accept folders 55 | # TYPE syncthing_rest_config_devices_is_remote_device_untrusted gauge 56 | syncthing_rest_config_devices_is_remote_device_untrusted{deviceID="EVOOQ2B-NZSX5XA-MGCOKJ7-M53F43K-HBKQOAS-7S5POY5-DSCKCTH-AI4T2AJ",name="host-1"} 0 57 | syncthing_rest_config_devices_is_remote_device_untrusted{deviceID="KXPN5AW-5EAJEA3-F7GXDVS-HPTV57U-NWDMFCS-SCTDFQF-CGGQTQD-FV2HAAZ",name="host-2"} 0 58 | # HELP syncthing_rest_config_devices_json_parse_failures Number of errors while parsing JSON. 59 | # TYPE syncthing_rest_config_devices_json_parse_failures counter 60 | syncthing_rest_config_devices_json_parse_failures 0 61 | # HELP syncthing_rest_config_devices_total_scrapes Current total Syncthings scrapes. 62 | # TYPE syncthing_rest_config_devices_total_scrapes counter 63 | syncthing_rest_config_devices_total_scrapes 1 64 | # HELP syncthing_rest_config_devices_up Was the last scrape of the Syncthing endpoint successful. 65 | # TYPE syncthing_rest_config_devices_up gauge 66 | syncthing_rest_config_devices_up 1 67 | ` 68 | err = testutil.CollectAndCompare( 69 | NewConfigDevicesReport(logger, HTTPClient, u, &testToken), 70 | strings.NewReader(expected), 71 | ) 72 | 73 | if err != nil { 74 | t.Errorf("NewConfigDevicesReport %s", err) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /collector/rest_db_status.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | "net/url" 9 | 10 | "github.com/prometheus/client_golang/prometheus" 11 | ) 12 | 13 | // DBStatusResponseBoolMetrics defines struct for boolean metrics 14 | type DBStatusResponseBoolMetrics struct { 15 | Type prometheus.ValueType 16 | Desc *prometheus.Desc 17 | Value func(v bool) float64 18 | } 19 | 20 | // DBStatusResponseNumericalMetrics defines struct for numeric metrics 21 | type DBStatusResponseNumericalMetrics struct { 22 | Type prometheus.ValueType 23 | Desc *prometheus.Desc 24 | Value float64 25 | } 26 | 27 | // DBStatusMetrics defines collector struct. 28 | type DBStatusMetrics struct { 29 | logger *slog.Logger 30 | client *http.Client 31 | url *url.URL 32 | token *string 33 | foldersid *[]string 34 | 35 | totalScrapes, jsonParseFailures prometheus.Counter 36 | boolMetrics map[string]*DBStatusResponseBoolMetrics 37 | numericalMetrics map[string]*DBStatusResponseNumericalMetrics 38 | } 39 | 40 | // NewDBStatusReport returns a new Collector exposing SVCResponse 41 | func NewDBStatusReport(logger *slog.Logger, client *http.Client, url *url.URL, token *string, foldersid *[]string) *DBStatusMetrics { 42 | subsystem := "rest_db_status" 43 | 44 | return &DBStatusMetrics{ 45 | logger: logger, 46 | client: client, 47 | url: url, 48 | token: token, 49 | foldersid: foldersid, 50 | 51 | totalScrapes: prometheus.NewCounter(prometheus.CounterOpts{ 52 | Name: prometheus.BuildFQName(namespace, subsystem, "total_scrapes"), 53 | Help: "Current total Syncthings scrapes.", 54 | }), 55 | jsonParseFailures: prometheus.NewCounter(prometheus.CounterOpts{ 56 | Name: prometheus.BuildFQName(namespace, subsystem, "json_parse_failures"), 57 | Help: "Number of errors while parsing JSON.", 58 | }), 59 | boolMetrics: map[string]*DBStatusResponseBoolMetrics{ 60 | "ignore_patterns": { 61 | Type: prometheus.GaugeValue, 62 | Desc: prometheus.NewDesc( 63 | prometheus.BuildFQName(namespace, subsystem, "ignore_patterns"), 64 | "Is using ignore patterns.", 65 | []string{"folderID"}, 66 | nil), 67 | Value: func(v bool) float64 { 68 | return bool2float64(v) 69 | }, 70 | }, 71 | }, 72 | 73 | numericalMetrics: map[string]*DBStatusResponseNumericalMetrics{ 74 | "errors": { 75 | Type: prometheus.GaugeValue, 76 | Desc: prometheus.NewDesc( 77 | prometheus.BuildFQName(namespace, subsystem, "errors"), 78 | "Number of errors for current folder.", 79 | []string{"folderID"}, 80 | nil), 81 | }, 82 | 83 | // Global section 84 | "global_bytes": { 85 | Type: prometheus.GaugeValue, 86 | Desc: prometheus.NewDesc( 87 | prometheus.BuildFQName(namespace, subsystem, "global_bytes"), 88 | "Number of bytes globally.", 89 | []string{"folderID"}, 90 | nil), 91 | }, 92 | "global_deleted": { 93 | Type: prometheus.GaugeValue, 94 | Desc: prometheus.NewDesc( 95 | prometheus.BuildFQName(namespace, subsystem, "global_deleted"), 96 | "Number of bytes deleted.", 97 | []string{"folderID"}, 98 | nil), 99 | }, 100 | "global_directories": { 101 | Type: prometheus.GaugeValue, 102 | Desc: prometheus.NewDesc( 103 | prometheus.BuildFQName(namespace, subsystem, "global_directories"), 104 | "Number of directories globally.", 105 | []string{"folderID"}, 106 | nil), 107 | }, 108 | "global_symlinks": { 109 | Type: prometheus.GaugeValue, 110 | Desc: prometheus.NewDesc( 111 | prometheus.BuildFQName(namespace, subsystem, "global_symlinks"), 112 | "Number of symlinks globally.", 113 | []string{"folderID"}, 114 | nil), 115 | }, 116 | "global_total_items": { 117 | Type: prometheus.GaugeValue, 118 | Desc: prometheus.NewDesc( 119 | prometheus.BuildFQName(namespace, subsystem, "global_total_items"), 120 | "Number of total items globally.", 121 | []string{"folderID"}, 122 | nil), 123 | }, 124 | 125 | // InSync section 126 | "insync_bytes": { 127 | Type: prometheus.GaugeValue, 128 | Desc: prometheus.NewDesc( 129 | prometheus.BuildFQName(namespace, subsystem, "insync_bytes"), 130 | "Number of bytes currently in sync.", 131 | []string{"folderID"}, 132 | nil), 133 | }, 134 | "insync_files": { 135 | Type: prometheus.GaugeValue, 136 | Desc: prometheus.NewDesc( 137 | prometheus.BuildFQName(namespace, subsystem, "insync_files"), 138 | "Number of files currently in sync.", 139 | []string{"folderID"}, 140 | nil), 141 | }, 142 | 143 | // Local section 144 | "local_bytes": { 145 | Type: prometheus.GaugeValue, 146 | Desc: prometheus.NewDesc( 147 | prometheus.BuildFQName(namespace, subsystem, "local_bytes"), 148 | "Number of bytes locally.", 149 | []string{"folderID"}, 150 | nil), 151 | }, 152 | "local_deleted": { 153 | Type: prometheus.GaugeValue, 154 | Desc: prometheus.NewDesc( 155 | prometheus.BuildFQName(namespace, subsystem, "local_deleted"), 156 | "Number of bytes deleted locally.", 157 | []string{"folderID"}, 158 | nil), 159 | }, 160 | "local_directories": { 161 | Type: prometheus.GaugeValue, 162 | Desc: prometheus.NewDesc( 163 | prometheus.BuildFQName(namespace, subsystem, "local_directories"), 164 | "Number of local directories.", 165 | []string{"folderID"}, 166 | nil), 167 | }, 168 | "local_symlinks": { 169 | Type: prometheus.GaugeValue, 170 | Desc: prometheus.NewDesc( 171 | prometheus.BuildFQName(namespace, subsystem, "local_symlinks"), 172 | "Number of local symlinks", 173 | []string{"folderID"}, 174 | nil), 175 | }, 176 | "local_total_items": { 177 | Type: prometheus.GaugeValue, 178 | Desc: prometheus.NewDesc( 179 | prometheus.BuildFQName(namespace, subsystem, "local_total_items"), 180 | "Number of total items locally", 181 | []string{"folderID"}, 182 | nil), 183 | }, 184 | 185 | // Need section 186 | "need_bytes": { 187 | Type: prometheus.GaugeValue, 188 | Desc: prometheus.NewDesc( 189 | prometheus.BuildFQName(namespace, subsystem, "need_bytes"), 190 | "Number of bytes need for sync.", 191 | []string{"folderID"}, 192 | nil), 193 | }, 194 | "need_deletes": { 195 | Type: prometheus.GaugeValue, 196 | Desc: prometheus.NewDesc( 197 | prometheus.BuildFQName(namespace, subsystem, "need_deletes"), 198 | "Number of bytes need for deletes.", 199 | []string{"folderID"}, 200 | nil), 201 | }, 202 | "need_directories": { 203 | Type: prometheus.GaugeValue, 204 | Desc: prometheus.NewDesc( 205 | prometheus.BuildFQName(namespace, subsystem, "need_directories"), 206 | "Number of directories for sync.", 207 | []string{"folderID"}, 208 | nil), 209 | }, 210 | "need_symlinks": { 211 | Type: prometheus.GaugeValue, 212 | Desc: prometheus.NewDesc( 213 | prometheus.BuildFQName(namespace, subsystem, "need_symlinks"), 214 | "Number of symlinks need for sync.", 215 | []string{"folderID"}, 216 | nil), 217 | }, 218 | "need_total_items": { 219 | Type: prometheus.GaugeValue, 220 | Desc: prometheus.NewDesc( 221 | prometheus.BuildFQName(namespace, subsystem, "need_total_items"), 222 | "Number of total items need to sync.", 223 | []string{"folderID"}, 224 | nil), 225 | }, 226 | 227 | // Misc section 228 | "pull_errors": { 229 | Type: prometheus.GaugeValue, 230 | Desc: prometheus.NewDesc( 231 | prometheus.BuildFQName(namespace, subsystem, "pull_errors"), 232 | "Number of pull errors.", 233 | []string{"folderID"}, 234 | nil), 235 | }, 236 | 237 | "sequence": { 238 | Type: prometheus.GaugeValue, 239 | Desc: prometheus.NewDesc( 240 | prometheus.BuildFQName(namespace, subsystem, "sequence"), 241 | "Total bytes received from remote device.", 242 | []string{"folderID", "state", "stateChanged"}, 243 | nil), 244 | }, 245 | }, 246 | } 247 | 248 | } 249 | 250 | // Describe set Prometheus metrics descriptions. 251 | func (c *DBStatusMetrics) Describe(ch chan<- *prometheus.Desc) { 252 | 253 | for _, metric := range c.boolMetrics { 254 | ch <- metric.Desc 255 | } 256 | 257 | for _, metric := range c.numericalMetrics { 258 | ch <- metric.Desc 259 | } 260 | 261 | ch <- c.totalScrapes.Desc() 262 | ch <- c.jsonParseFailures.Desc() 263 | } 264 | 265 | func (c *DBStatusMetrics) fetchDataAndDecode() map[string]DBStatusResponse { 266 | 267 | var chr DBStatusResponse 268 | data := make(map[string]DBStatusResponse) 269 | var message string 270 | 271 | for indexfolderid := range *c.foldersid { 272 | 273 | c.totalScrapes.Inc() 274 | u := *c.url 275 | folderid := (*c.foldersid)[indexfolderid] 276 | url, _ := u.Parse(fmt.Sprintf("/rest/db/status?folder=%s", folderid)) 277 | 278 | h := make(http.Header) 279 | h["X-API-Key"] = []string{*c.token} 280 | 281 | request := &http.Request{ 282 | URL: url, 283 | Header: h, 284 | } 285 | 286 | res, err := c.client.Do(request) 287 | if err != nil { 288 | message = fmt.Sprintf("Request %s://%s%s%s: failed with code %s", 289 | request.URL.Scheme, request.URL.Host, request.URL.Path, request.URL.RawQuery, err) 290 | c.logger.Error(message) 291 | continue 292 | } 293 | 294 | defer func() { 295 | err = res.Body.Close() 296 | if err != nil { 297 | c.logger.Info(fmt.Sprintf("%s: %s", "Failed to close http.Client", err)) 298 | } 299 | }() 300 | 301 | if res.StatusCode != http.StatusOK { 302 | message = fmt.Sprintf("Request %s://%s%s%s: failed with code %d", 303 | request.URL.Scheme, request.URL.Host, request.URL.Path, request.URL.RawQuery, res.StatusCode) 304 | c.logger.Error(message) 305 | continue 306 | } 307 | 308 | if err := json.NewDecoder(res.Body).Decode(&chr); err != nil { 309 | c.logger.Info(fmt.Sprintf("%s: %s", "Failed decode json", err)) 310 | c.jsonParseFailures.Inc() 311 | continue 312 | } 313 | data[folderid] = chr 314 | } 315 | return data 316 | } 317 | 318 | // Collect collects Syncthing metrics from /rest/db/status. 319 | func (c *DBStatusMetrics) Collect(ch chan<- prometheus.Metric) { 320 | 321 | defer func() { 322 | ch <- c.totalScrapes 323 | ch <- c.jsonParseFailures 324 | }() 325 | 326 | response := c.fetchDataAndDecode() 327 | 328 | for folderid := range response { 329 | 330 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["errors"].Desc, c.numericalMetrics["errors"].Type, response[folderid].Errors, folderid) 331 | 332 | // Global section 333 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["global_bytes"].Desc, c.numericalMetrics["global_bytes"].Type, response[folderid].GlobalBytes, folderid) 334 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["global_deleted"].Desc, c.numericalMetrics["global_deleted"].Type, response[folderid].GlobalDeleted, folderid) 335 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["global_directories"].Desc, c.numericalMetrics["global_directories"].Type, response[folderid].GlobalDirectories, folderid) 336 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["global_symlinks"].Desc, c.numericalMetrics["global_symlinks"].Type, response[folderid].GlobalSymlinks, folderid) 337 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["global_total_items"].Desc, c.numericalMetrics["global_total_items"].Type, response[folderid].GlobalTotalItems, folderid) 338 | 339 | // Bool metrics 340 | ch <- prometheus.MustNewConstMetric( 341 | c.boolMetrics["ignore_patterns"].Desc, 342 | c.boolMetrics["ignore_patterns"].Type, 343 | c.boolMetrics["ignore_patterns"].Value(response[folderid].IgnorePatters), 344 | folderid, 345 | ) 346 | 347 | // InSync section 348 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["insync_bytes"].Desc, c.numericalMetrics["insync_bytes"].Type, response[folderid].InSyncBytes, folderid) 349 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["insync_files"].Desc, c.numericalMetrics["insync_files"].Type, response[folderid].InSyncFiles, folderid) 350 | 351 | // Local section 352 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["local_bytes"].Desc, c.numericalMetrics["local_bytes"].Type, response[folderid].LocalBytes, folderid) 353 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["local_deleted"].Desc, c.numericalMetrics["local_deleted"].Type, response[folderid].LocalDeleted, folderid) 354 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["local_directories"].Desc, c.numericalMetrics["local_directories"].Type, response[folderid].LocalDirectories, folderid) 355 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["local_symlinks"].Desc, c.numericalMetrics["local_symlinks"].Type, response[folderid].LocalSymlinks, folderid) 356 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["local_total_items"].Desc, c.numericalMetrics["local_total_items"].Type, response[folderid].LocalTotalItems, folderid) 357 | 358 | // Need section 359 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["need_bytes"].Desc, c.numericalMetrics["need_bytes"].Type, response[folderid].NeedBytes, folderid) 360 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["need_deletes"].Desc, c.numericalMetrics["need_deletes"].Type, response[folderid].NeedDeletes, folderid) 361 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["need_directories"].Desc, c.numericalMetrics["need_directories"].Type, response[folderid].NeedDirectories, folderid) 362 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["need_symlinks"].Desc, c.numericalMetrics["need_symlinks"].Type, response[folderid].NeedSymlinks, folderid) 363 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["need_total_items"].Desc, c.numericalMetrics["need_total_items"].Type, response[folderid].NeedTotalItems, folderid) 364 | 365 | // Misc section 366 | ch <- prometheus.MustNewConstMetric(c.numericalMetrics["pull_errors"].Desc, c.numericalMetrics["pull_errors"].Type, response[folderid].PullErrors, folderid) 367 | ch <- prometheus.MustNewConstMetric( 368 | c.numericalMetrics["sequence"].Desc, 369 | c.numericalMetrics["sequence"].Type, 370 | response[folderid].Sequence, 371 | folderid, response[folderid].State, response[folderid].StateChanged, 372 | ) 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /collector/rest_db_status_response.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | // DBStatusResponse is a struct of the `syncthing` instance DB status 4 | type DBStatusResponse struct { 5 | Errors float64 `json:"errors"` 6 | 7 | GlobalBytes float64 `json:"globalBytes"` 8 | GlobalDeleted float64 `json:"globalDeleted"` 9 | GlobalDirectories float64 `json:"globalDirectories"` 10 | GlobalSymlinks float64 `json:"globalSymlinks"` 11 | GlobalTotalItems float64 `json:"globalTotalItems"` 12 | 13 | IgnorePatters bool `json:"ignorePatterns"` 14 | 15 | InSyncBytes float64 `json:"inSyncBytes"` 16 | InSyncFiles float64 `json:"inSyncFiles"` 17 | 18 | Invalid string `json:"invalid,omitempty"` // Deprecated, retains external API for now 19 | 20 | LocalBytes float64 `json:"localBytes"` 21 | LocalDeleted float64 `json:"localDeleted"` 22 | LocalDirectories float64 `json:"localDirectories"` 23 | LocalFiles float64 `json:"localFiles"` 24 | LocalSymlinks float64 `json:"localSymlinks"` 25 | LocalTotalItems float64 `json:"localTotalItems"` 26 | 27 | NeedBytes float64 `json:"needBytes"` 28 | NeedDeletes float64 `json:"needDeletes"` 29 | NeedDirectories float64 `json:"needDirectories"` 30 | NeedFiles float64 `json:"needFiles"` 31 | NeedSymlinks float64 `json:"needSymlinks"` 32 | NeedTotalItems float64 `json:"needTotalItems"` 33 | 34 | PullErrors float64 `json:"pullErrors"` 35 | Sequence float64 `json:"sequence"` 36 | 37 | State string `json:"state"` 38 | StateChanged string `json:"stateChanged"` 39 | Version float64 `json:"version,omitempty"` // Deprecated, retains external API for now 40 | 41 | } 42 | -------------------------------------------------------------------------------- /collector/rest_db_status_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "os" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/prometheus/client_golang/prometheus/testutil" 13 | "github.com/prometheus/common/promslog" 14 | ) 15 | 16 | func TestNewDBStatusReport(t *testing.T) { 17 | 18 | jsonResponse, _ := os.ReadFile("fixtures/rest_db_status_response.json") 19 | 20 | ts := httptest.NewTLSServer( 21 | http.HandlerFunc( 22 | func(w http.ResponseWriter, _ *http.Request) { 23 | fmt.Fprintln(w, string(jsonResponse)) 24 | }, 25 | ), 26 | ) 27 | defer ts.Close() 28 | 29 | u, err := url.Parse(ts.URL) 30 | if err != nil { 31 | t.Errorf("url parse error: %s", err) 32 | } 33 | 34 | promlogConfig := &promslog.Config{} 35 | logger := promslog.New(promlogConfig) 36 | 37 | testToken := "12345" 38 | testFoldersid := []string{"aaaaa-bb11b"} 39 | 40 | expected := ` 41 | # HELP syncthing_rest_db_status_errors Number of errors for current folder. 42 | # TYPE syncthing_rest_db_status_errors gauge 43 | syncthing_rest_db_status_errors{folderID="aaaaa-bb11b"} 0 44 | # HELP syncthing_rest_db_status_global_bytes Number of bytes globally. 45 | # TYPE syncthing_rest_db_status_global_bytes gauge 46 | syncthing_rest_db_status_global_bytes{folderID="aaaaa-bb11b"} 4.047955455069e+12 47 | # HELP syncthing_rest_db_status_global_deleted Number of bytes deleted. 48 | # TYPE syncthing_rest_db_status_global_deleted gauge 49 | syncthing_rest_db_status_global_deleted{folderID="aaaaa-bb11b"} 156112 50 | # HELP syncthing_rest_db_status_global_directories Number of directories globally. 51 | # TYPE syncthing_rest_db_status_global_directories gauge 52 | syncthing_rest_db_status_global_directories{folderID="aaaaa-bb11b"} 346372 53 | # HELP syncthing_rest_db_status_global_symlinks Number of symlinks globally. 54 | # TYPE syncthing_rest_db_status_global_symlinks gauge 55 | syncthing_rest_db_status_global_symlinks{folderID="aaaaa-bb11b"} 0 56 | # HELP syncthing_rest_db_status_global_total_items Number of total items globally. 57 | # TYPE syncthing_rest_db_status_global_total_items gauge 58 | syncthing_rest_db_status_global_total_items{folderID="aaaaa-bb11b"} 1.197108e+06 59 | # HELP syncthing_rest_db_status_ignore_patterns Is using ignore patterns. 60 | # TYPE syncthing_rest_db_status_ignore_patterns gauge 61 | syncthing_rest_db_status_ignore_patterns{folderID="aaaaa-bb11b"} 0 62 | # HELP syncthing_rest_db_status_insync_bytes Number of bytes currently in sync. 63 | # TYPE syncthing_rest_db_status_insync_bytes gauge 64 | syncthing_rest_db_status_insync_bytes{folderID="aaaaa-bb11b"} 4.047955455069e+12 65 | # HELP syncthing_rest_db_status_insync_files Number of files currently in sync. 66 | # TYPE syncthing_rest_db_status_insync_files gauge 67 | syncthing_rest_db_status_insync_files{folderID="aaaaa-bb11b"} 694624 68 | # HELP syncthing_rest_db_status_json_parse_failures Number of errors while parsing JSON. 69 | # TYPE syncthing_rest_db_status_json_parse_failures counter 70 | syncthing_rest_db_status_json_parse_failures 0 71 | # HELP syncthing_rest_db_status_local_bytes Number of bytes locally. 72 | # TYPE syncthing_rest_db_status_local_bytes gauge 73 | syncthing_rest_db_status_local_bytes{folderID="aaaaa-bb11b"} 4.047955455069e+12 74 | # HELP syncthing_rest_db_status_local_deleted Number of bytes deleted locally. 75 | # TYPE syncthing_rest_db_status_local_deleted gauge 76 | syncthing_rest_db_status_local_deleted{folderID="aaaaa-bb11b"} 318 77 | # HELP syncthing_rest_db_status_local_directories Number of local directories. 78 | # TYPE syncthing_rest_db_status_local_directories gauge 79 | syncthing_rest_db_status_local_directories{folderID="aaaaa-bb11b"} 346372 80 | # HELP syncthing_rest_db_status_local_symlinks Number of local symlinks 81 | # TYPE syncthing_rest_db_status_local_symlinks gauge 82 | syncthing_rest_db_status_local_symlinks{folderID="aaaaa-bb11b"} 0 83 | # HELP syncthing_rest_db_status_local_total_items Number of total items locally 84 | # TYPE syncthing_rest_db_status_local_total_items gauge 85 | syncthing_rest_db_status_local_total_items{folderID="aaaaa-bb11b"} 1.041314e+06 86 | # HELP syncthing_rest_db_status_need_bytes Number of bytes need for sync. 87 | # TYPE syncthing_rest_db_status_need_bytes gauge 88 | syncthing_rest_db_status_need_bytes{folderID="aaaaa-bb11b"} 0 89 | # HELP syncthing_rest_db_status_need_deletes Number of bytes need for deletes. 90 | # TYPE syncthing_rest_db_status_need_deletes gauge 91 | syncthing_rest_db_status_need_deletes{folderID="aaaaa-bb11b"} 0 92 | # HELP syncthing_rest_db_status_need_directories Number of directories for sync. 93 | # TYPE syncthing_rest_db_status_need_directories gauge 94 | syncthing_rest_db_status_need_directories{folderID="aaaaa-bb11b"} 0 95 | # HELP syncthing_rest_db_status_need_symlinks Number of symlinks need for sync. 96 | # TYPE syncthing_rest_db_status_need_symlinks gauge 97 | syncthing_rest_db_status_need_symlinks{folderID="aaaaa-bb11b"} 0 98 | # HELP syncthing_rest_db_status_need_total_items Number of total items need to sync. 99 | # TYPE syncthing_rest_db_status_need_total_items gauge 100 | syncthing_rest_db_status_need_total_items{folderID="aaaaa-bb11b"} 0 101 | # HELP syncthing_rest_db_status_pull_errors Number of pull errors. 102 | # TYPE syncthing_rest_db_status_pull_errors gauge 103 | syncthing_rest_db_status_pull_errors{folderID="aaaaa-bb11b"} 0 104 | # HELP syncthing_rest_db_status_sequence Total bytes received from remote device. 105 | # TYPE syncthing_rest_db_status_sequence gauge 106 | syncthing_rest_db_status_sequence{folderID="aaaaa-bb11b",state="idle",stateChanged="2077-02-06T00:02:00-07:00"} 1.213411e+06 107 | # HELP syncthing_rest_db_status_total_scrapes Current total Syncthings scrapes. 108 | # TYPE syncthing_rest_db_status_total_scrapes counter 109 | syncthing_rest_db_status_total_scrapes 1 110 | ` 111 | 112 | err = testutil.CollectAndCompare( 113 | NewDBStatusReport(logger, HTTPClient, u, &testToken, &testFoldersid), 114 | strings.NewReader(expected), 115 | ) 116 | 117 | if err != nil { 118 | t.Errorf("NewDBStatusReport %s", err) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /collector/rest_stats_device.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | "net/url" 9 | "time" 10 | 11 | "github.com/prometheus/client_golang/prometheus" 12 | ) 13 | 14 | // StatsDeviceResponseNumericalMetrics defines struct for numeric metrics 15 | type StatsDeviceResponseNumericalMetrics struct { 16 | Type prometheus.ValueType 17 | Desc *prometheus.Desc 18 | Value func(v float64) float64 19 | } 20 | 21 | // StatsDeviceResponse defines collector struct. 22 | type StatsDeviceResponse struct { 23 | logger *slog.Logger 24 | client *http.Client 25 | url *url.URL 26 | token *string 27 | 28 | up prometheus.Gauge 29 | totalScrapes, jsonParseFailures prometheus.Counter 30 | numericalMetrics map[string]*StatsDeviceResponseNumericalMetrics 31 | } 32 | 33 | // NewStatsDeviceReport generate report from device 34 | func NewStatsDeviceReport(logger *slog.Logger, client *http.Client, url *url.URL, token *string) *StatsDeviceResponse { 35 | subsystem := "rest_stats_device" 36 | 37 | return &StatsDeviceResponse{ 38 | logger: logger, 39 | client: client, 40 | url: url, 41 | token: token, 42 | 43 | up: prometheus.NewGauge(prometheus.GaugeOpts{ 44 | Name: prometheus.BuildFQName(namespace, subsystem, "up"), 45 | Help: "Was the last scrape of the Syncting system connections endpoint successful.", 46 | }), 47 | totalScrapes: prometheus.NewCounter(prometheus.CounterOpts{ 48 | Name: prometheus.BuildFQName(namespace, subsystem, "total_scrapes"), 49 | Help: "Current total Syncthings scrapes.", 50 | }), 51 | jsonParseFailures: prometheus.NewCounter(prometheus.CounterOpts{ 52 | Name: prometheus.BuildFQName(namespace, subsystem, "json_parse_failures"), 53 | Help: "Number of errors while parsing JSON.", 54 | }), 55 | numericalMetrics: map[string]*StatsDeviceResponseNumericalMetrics{ 56 | "last_connection_duration": { 57 | Type: prometheus.GaugeValue, 58 | Desc: prometheus.NewDesc( 59 | prometheus.BuildFQName(namespace, subsystem, "last_connection_duration"), 60 | "Duration of last connection with remote device in seconds. If value less 0, means value was missed in syncthing response.", 61 | []string{"deviceID"}, nil), 62 | Value: func(v float64) float64 { 63 | return v 64 | }, 65 | }, 66 | "last_connection_timestamp": { 67 | Type: prometheus.GaugeValue, 68 | Desc: prometheus.NewDesc( 69 | prometheus.BuildFQName(namespace, subsystem, "last_connection_timestamp"), 70 | "Timestamp since last connection with remote device expressed in Unix epoch", 71 | []string{"deviceID"}, nil), 72 | Value: func(v float64) float64 { 73 | return v 74 | }, 75 | }, 76 | }, 77 | } 78 | 79 | } 80 | 81 | // Describe set Prometheus metrics descriptions. 82 | func (c *StatsDeviceResponse) Describe(ch chan<- *prometheus.Desc) { 83 | 84 | for _, metric := range c.numericalMetrics { 85 | ch <- metric.Desc 86 | } 87 | 88 | ch <- c.up.Desc() 89 | ch <- c.totalScrapes.Desc() 90 | ch <- c.jsonParseFailures.Desc() 91 | } 92 | 93 | func (c *StatsDeviceResponse) fetchDataAndDecode() (map[string]interface{}, error) { 94 | chr := make(map[string]interface{}) 95 | 96 | u := *c.url 97 | url, _ := u.Parse("/rest/stats/device") 98 | 99 | h := make(http.Header) 100 | h["X-API-Key"] = []string{*c.token} 101 | 102 | request := &http.Request{ 103 | URL: url, 104 | Header: h, 105 | } 106 | 107 | res, err := c.client.Do(request) 108 | if err != nil { 109 | return chr, fmt.Errorf("failed to get data from %s://%s:%s%s: %s", 110 | u.Scheme, u.Hostname(), u.Port(), u.Path, err) 111 | } 112 | 113 | defer func() { 114 | err = res.Body.Close() 115 | if err != nil { 116 | c.logger.Warn(fmt.Sprintf("%s: %s", "failed to close http.Client", err)) 117 | } 118 | }() 119 | 120 | if res.StatusCode != http.StatusOK { 121 | return chr, fmt.Errorf("HTTP Request failed with code %d", res.StatusCode) 122 | } 123 | 124 | if err := json.NewDecoder(res.Body).Decode(&chr); err != nil { 125 | c.jsonParseFailures.Inc() 126 | return chr, err 127 | } 128 | 129 | return chr, nil 130 | 131 | } 132 | 133 | // Collect collects Syncthing metrics from /rest/stats/device. 134 | func (c *StatsDeviceResponse) Collect(ch chan<- prometheus.Metric) { 135 | var err error 136 | 137 | c.totalScrapes.Inc() 138 | defer func() { 139 | ch <- c.up 140 | ch <- c.totalScrapes 141 | ch <- c.jsonParseFailures 142 | }() 143 | statsDeviceResponse, err := c.fetchDataAndDecode() 144 | if err != nil { 145 | c.up.Set(0) 146 | c.logger.Info(fmt.Sprintf("%s: %s", "failed to fetch and decode data", err)) 147 | return 148 | } 149 | c.up.Set(1) 150 | 151 | for deviceID, deviceData := range statsDeviceResponse { 152 | deviceDataAssertion := deviceData.(map[string]interface{}) 153 | 154 | if deviceDataAssertion["lastConnectionDurationS"] == nil { 155 | deviceDataAssertion["lastConnectionDurationS"] = -1.0 156 | } 157 | 158 | ch <- prometheus.MustNewConstMetric( 159 | c.numericalMetrics["last_connection_duration"].Desc, 160 | c.numericalMetrics["last_connection_duration"].Type, 161 | c.numericalMetrics["last_connection_duration"].Value(deviceDataAssertion["lastConnectionDurationS"].(float64)), 162 | deviceID, 163 | ) 164 | thetime, err := time.Parse(time.RFC3339, deviceDataAssertion["lastSeen"].(string)) 165 | if err != nil { 166 | c.logger.Warn(fmt.Sprintf("%s: %s", "failed to parse timestamp", err)) 167 | return 168 | } 169 | ch <- prometheus.MustNewConstMetric( 170 | c.numericalMetrics["last_connection_timestamp"].Desc, 171 | c.numericalMetrics["last_connection_timestamp"].Type, 172 | c.numericalMetrics["last_connection_timestamp"].Value(float64(thetime.Unix())), 173 | deviceID, 174 | ) 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /collector/rest_stats_device_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "os" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/prometheus/client_golang/prometheus/testutil" 13 | "github.com/prometheus/common/promslog" 14 | ) 15 | 16 | func TestNewStatsDeviceReport(t *testing.T) { 17 | 18 | jsonResponse, _ := os.ReadFile("fixtures/rest_stats_device_response.json") 19 | 20 | ts := httptest.NewTLSServer( 21 | http.HandlerFunc( 22 | func(w http.ResponseWriter, _ *http.Request) { 23 | fmt.Fprintln(w, string(jsonResponse)) 24 | }, 25 | ), 26 | ) 27 | defer ts.Close() 28 | 29 | u, err := url.Parse(ts.URL) 30 | if err != nil { 31 | t.Errorf("url parse error: %s", err) 32 | } 33 | 34 | promslogConfig := &promslog.Config{} 35 | logger := promslog.New(promslogConfig) 36 | 37 | testToken := "12345" 38 | expected := ` 39 | # HELP syncthing_rest_stats_device_json_parse_failures Number of errors while parsing JSON. 40 | # TYPE syncthing_rest_stats_device_json_parse_failures counter 41 | syncthing_rest_stats_device_json_parse_failures 0 42 | # HELP syncthing_rest_stats_device_last_connection_duration Duration of last connection with remote device in seconds. If value less 0, means value was missed in syncthing response. 43 | # TYPE syncthing_rest_stats_device_last_connection_duration gauge 44 | syncthing_rest_stats_device_last_connection_duration{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA"} 0 45 | syncthing_rest_stats_device_last_connection_duration{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB"} 819990.3432191 46 | syncthing_rest_stats_device_last_connection_duration{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC"} 0 47 | # HELP syncthing_rest_stats_device_last_connection_timestamp Timestamp since last connection with remote device expressed in Unix epoch 48 | # TYPE syncthing_rest_stats_device_last_connection_timestamp gauge 49 | syncthing_rest_stats_device_last_connection_timestamp{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA"} 3.40704005e+09 50 | syncthing_rest_stats_device_last_connection_timestamp{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB"} 3.385540225e+09 51 | syncthing_rest_stats_device_last_connection_timestamp{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC"} 4.733856e+08 52 | # HELP syncthing_rest_stats_device_total_scrapes Current total Syncthings scrapes. 53 | # TYPE syncthing_rest_stats_device_total_scrapes counter 54 | syncthing_rest_stats_device_total_scrapes 1 55 | # HELP syncthing_rest_stats_device_up Was the last scrape of the Syncting system connections endpoint successful. 56 | # TYPE syncthing_rest_stats_device_up gauge 57 | syncthing_rest_stats_device_up 1 58 | ` 59 | 60 | err = testutil.CollectAndCompare( 61 | NewStatsDeviceReport(logger, HTTPClient, u, &testToken), 62 | strings.NewReader(expected), 63 | ) 64 | 65 | if err != nil { 66 | t.Errorf("NewStatsDeviceReportError %s", err) 67 | } 68 | } 69 | 70 | func TestStatsDeviceReportWithNoLastConnectionDuration(t *testing.T) { 71 | 72 | jsonResponse, _ := os.ReadFile("fixtures/rest_stats_device_response_missed_lastConnectionDurationS.json") 73 | 74 | ts := httptest.NewTLSServer( 75 | http.HandlerFunc( 76 | func(w http.ResponseWriter, _ *http.Request) { 77 | fmt.Fprintln(w, string(jsonResponse)) 78 | }, 79 | ), 80 | ) 81 | 82 | defer ts.Close() 83 | 84 | u, err := url.Parse(ts.URL) 85 | if err != nil { 86 | t.Errorf("url parse error: %s", err) 87 | } 88 | 89 | promslogConfig := &promslog.Config{} 90 | logger := promslog.New(promslogConfig) 91 | 92 | testToken := "12345" 93 | expected := ` 94 | # HELP syncthing_rest_stats_device_json_parse_failures Number of errors while parsing JSON. 95 | # TYPE syncthing_rest_stats_device_json_parse_failures counter 96 | syncthing_rest_stats_device_json_parse_failures 0 97 | # HELP syncthing_rest_stats_device_last_connection_duration Duration of last connection with remote device in seconds. If value less 0, means value was missed in syncthing response. 98 | # TYPE syncthing_rest_stats_device_last_connection_duration gauge 99 | syncthing_rest_stats_device_last_connection_duration{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA"} 0 100 | syncthing_rest_stats_device_last_connection_duration{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB"} 819990.3432191 101 | syncthing_rest_stats_device_last_connection_duration{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC"} -1 102 | # HELP syncthing_rest_stats_device_last_connection_timestamp Timestamp since last connection with remote device expressed in Unix epoch 103 | # TYPE syncthing_rest_stats_device_last_connection_timestamp gauge 104 | syncthing_rest_stats_device_last_connection_timestamp{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA"} 3.40704005e+09 105 | syncthing_rest_stats_device_last_connection_timestamp{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB"} 3.385540225e+09 106 | syncthing_rest_stats_device_last_connection_timestamp{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC"} 4.733856e+08 107 | # HELP syncthing_rest_stats_device_total_scrapes Current total Syncthings scrapes. 108 | # TYPE syncthing_rest_stats_device_total_scrapes counter 109 | syncthing_rest_stats_device_total_scrapes 1 110 | # HELP syncthing_rest_stats_device_up Was the last scrape of the Syncting system connections endpoint successful. 111 | # TYPE syncthing_rest_stats_device_up gauge 112 | syncthing_rest_stats_device_up 1 113 | ` 114 | 115 | err = testutil.CollectAndCompare( 116 | NewStatsDeviceReport(logger, HTTPClient, u, &testToken), 117 | strings.NewReader(expected), 118 | ) 119 | 120 | if err != nil { 121 | t.Errorf("NewStatsDeviceReportError %s", err) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /collector/rest_svc_report.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | "net/url" 9 | 10 | "github.com/prometheus/client_golang/prometheus" 11 | "github.com/syncthing/syncthing/lib/ur/contract" 12 | ) 13 | 14 | type svcMetric struct { 15 | Type prometheus.ValueType 16 | Desc *prometheus.Desc 17 | Value func(svcMetric *contract.Report) float64 18 | } 19 | 20 | type svcData struct { 21 | Type prometheus.ValueType 22 | Desc *prometheus.Desc 23 | Value func(svcData *contract.Report) float64 24 | } 25 | 26 | // SVCResponse defines collector struct. 27 | type SVCResponse struct { 28 | logger *slog.Logger 29 | client *http.Client 30 | url *url.URL 31 | token *string 32 | 33 | up prometheus.Gauge 34 | totalScrapes, jsonParseFailures prometheus.Counter 35 | metrics []*svcMetric 36 | data *svcData 37 | } 38 | 39 | // NewSVCReport returns a new Collector exposing SVCResponse 40 | func NewSVCReport(logger *slog.Logger, client *http.Client, url *url.URL, token *string) *SVCResponse { 41 | subsystem := "rest_svc_report" 42 | 43 | return &SVCResponse{ 44 | logger: logger, 45 | client: client, 46 | url: url, 47 | token: token, 48 | 49 | up: prometheus.NewGauge(prometheus.GaugeOpts{ 50 | Name: prometheus.BuildFQName(namespace, subsystem, "up"), 51 | Help: "Was the last scrape of the Syncthing endpoint successful.", 52 | }), 53 | totalScrapes: prometheus.NewCounter(prometheus.CounterOpts{ 54 | Name: prometheus.BuildFQName(namespace, subsystem, "total_scrapes"), 55 | Help: "Current total Syncthings scrapes.", 56 | }), 57 | jsonParseFailures: prometheus.NewCounter(prometheus.CounterOpts{ 58 | Name: prometheus.BuildFQName(namespace, subsystem, "json_parse_failures"), 59 | Help: "Number of errors while parsing JSON.", 60 | }), 61 | 62 | metrics: []*svcMetric{ 63 | { 64 | Type: prometheus.GaugeValue, 65 | Desc: prometheus.NewDesc( 66 | prometheus.BuildFQName(namespace, subsystem, "number_folders"), 67 | "Number of folders in sync", nil, nil, 68 | ), 69 | Value: func(svcMetric *contract.Report) float64 { 70 | return float64(svcMetric.NumFolders) 71 | }, 72 | }, 73 | { 74 | Type: prometheus.GaugeValue, 75 | Desc: prometheus.NewDesc( 76 | prometheus.BuildFQName(namespace, subsystem, "number_devices"), 77 | "Number of devices in sync", nil, nil, 78 | ), 79 | Value: func(svcMetric *contract.Report) float64 { 80 | return float64(svcMetric.NumDevices) 81 | }, 82 | }, 83 | { 84 | Type: prometheus.GaugeValue, 85 | Desc: prometheus.NewDesc( 86 | prometheus.BuildFQName(namespace, subsystem, "total_files"), 87 | "Total number of files", nil, nil, 88 | ), 89 | Value: func(svcMetric *contract.Report) float64 { 90 | return float64(svcMetric.TotFiles) 91 | }, 92 | }, 93 | { 94 | Type: prometheus.GaugeValue, 95 | Desc: prometheus.NewDesc( 96 | prometheus.BuildFQName(namespace, subsystem, "total_data_in_MB"), 97 | "Total data in megabytes", nil, nil, 98 | ), 99 | Value: func(svcMetric *contract.Report) float64 { 100 | return float64(svcMetric.TotMiB) 101 | }, 102 | }, 103 | { 104 | Type: prometheus.GaugeValue, 105 | Desc: prometheus.NewDesc( 106 | prometheus.BuildFQName(namespace, subsystem, "memory_usage_mb"), 107 | "Memory usage by syncthc in MB", nil, nil, 108 | ), 109 | Value: func(svcMetric *contract.Report) float64 { 110 | return float64(svcMetric.MemoryUsageMiB) 111 | }, 112 | }, 113 | { 114 | Type: prometheus.GaugeValue, 115 | Desc: prometheus.NewDesc( 116 | prometheus.BuildFQName(namespace, subsystem, "sha256_performance"), 117 | "SHA256 Performance value", nil, nil, 118 | ), 119 | Value: func(svcMetric *contract.Report) float64 { 120 | return float64(svcMetric.SHA256Perf) 121 | }, 122 | }, 123 | { 124 | Type: prometheus.GaugeValue, 125 | Desc: prometheus.NewDesc( 126 | prometheus.BuildFQName(namespace, subsystem, "hash_performance"), 127 | "Hash Performance value", nil, nil, 128 | ), 129 | Value: func(svcMetric *contract.Report) float64 { 130 | return float64(svcMetric.HashPerf) 131 | }, 132 | }, 133 | { 134 | Type: prometheus.GaugeValue, 135 | Desc: prometheus.NewDesc( 136 | prometheus.BuildFQName(namespace, subsystem, "memory_size"), 137 | "Node memory size in megabytes", nil, nil, 138 | ), 139 | Value: func(svcMetric *contract.Report) float64 { 140 | return float64(svcMetric.MemorySize) 141 | }, 142 | }, 143 | { 144 | Type: prometheus.GaugeValue, 145 | Desc: prometheus.NewDesc( 146 | prometheus.BuildFQName(namespace, subsystem, "num_cpu"), 147 | "Number of node CPU", nil, nil, 148 | ), 149 | Value: func(svcMetric *contract.Report) float64 { 150 | return float64(svcMetric.NumCPU) 151 | }, 152 | }, 153 | { 154 | Type: prometheus.GaugeValue, 155 | Desc: prometheus.NewDesc( 156 | prometheus.BuildFQName(namespace, subsystem, "uptime"), 157 | "Syncthing uptime in seconds", nil, nil, 158 | ), 159 | Value: func(svcMetric *contract.Report) float64 { 160 | return float64(svcMetric.Uptime) 161 | }, 162 | }, 163 | }, 164 | 165 | data: &svcData{ 166 | Type: prometheus.GaugeValue, 167 | Desc: prometheus.NewDesc( 168 | prometheus.BuildFQName(namespace, subsystem, "node_info"), 169 | "Node's string information", 170 | []string{"uniqueID", "version", "longVersion", "platform"}, 171 | nil, 172 | ), 173 | Value: func(_ *contract.Report) float64 { 174 | // Leave this parameter for backward compatibility with versions < v0.3.0 of grafana dashboards 175 | return float64(1) 176 | }, 177 | }, 178 | } 179 | 180 | } 181 | 182 | // Describe set Prometheus metrics descriptions. 183 | func (c *SVCResponse) Describe(ch chan<- *prometheus.Desc) { 184 | for _, metric := range c.metrics { 185 | ch <- metric.Desc 186 | } 187 | ch <- c.data.Desc 188 | ch <- c.up.Desc() 189 | ch <- c.totalScrapes.Desc() 190 | ch <- c.jsonParseFailures.Desc() 191 | } 192 | 193 | func (c *SVCResponse) fetchDataAndDecode() (contract.Report, error) { 194 | var chr contract.Report 195 | 196 | u := *c.url 197 | url, _ := u.Parse("/rest/svc/report") 198 | 199 | h := make(http.Header) 200 | h["X-API-Key"] = []string{*c.token} 201 | 202 | request := &http.Request{ 203 | URL: url, 204 | Header: h, 205 | } 206 | 207 | res, err := c.client.Do(request) 208 | if err != nil { 209 | return chr, fmt.Errorf("failed to get data from %s://%s:%s%s: %s", 210 | u.Scheme, u.Hostname(), u.Port(), u.Path, err) 211 | } 212 | 213 | defer func() { 214 | err = res.Body.Close() 215 | if err != nil { 216 | c.logger.Warn(fmt.Sprintf("%s: %s", "failed to close http.Client", err)) 217 | } 218 | }() 219 | 220 | if res.StatusCode != http.StatusOK { 221 | return chr, fmt.Errorf("HTTP Request failed with code %d", res.StatusCode) 222 | } 223 | 224 | if err := json.NewDecoder(res.Body).Decode(&chr); err != nil { 225 | c.jsonParseFailures.Inc() 226 | return chr, err 227 | } 228 | 229 | return chr, nil 230 | } 231 | 232 | // Collect collects Syncthing metrics from /rest/svc/report. 233 | func (c *SVCResponse) Collect(ch chan<- prometheus.Metric) { 234 | var err error 235 | c.totalScrapes.Inc() 236 | defer func() { 237 | ch <- c.up 238 | ch <- c.totalScrapes 239 | ch <- c.jsonParseFailures 240 | }() 241 | 242 | SVCResp, err := c.fetchDataAndDecode() 243 | if err != nil { 244 | c.up.Set(0) 245 | c.logger.Warn(fmt.Sprintf("%s: %s", "failed to fetch and decode data", err)) 246 | return 247 | } 248 | c.up.Set(1) 249 | 250 | for _, metric := range c.metrics { 251 | ch <- prometheus.MustNewConstMetric( 252 | metric.Desc, 253 | metric.Type, 254 | metric.Value(&SVCResp), 255 | ) 256 | } 257 | 258 | ch <- prometheus.MustNewConstMetric( 259 | c.data.Desc, 260 | c.data.Type, 261 | c.data.Value(&SVCResp), 262 | SVCResp.UniqueID, SVCResp.Version, SVCResp.LongVersion, SVCResp.Platform, 263 | ) 264 | } 265 | -------------------------------------------------------------------------------- /collector/rest_svc_report_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "os" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/prometheus/client_golang/prometheus/testutil" 13 | "github.com/prometheus/common/promslog" 14 | ) 15 | 16 | func TestNewSVCReport(t *testing.T) { 17 | 18 | jsonResponse, _ := os.ReadFile("fixtures/rest_svc_report_response.json") 19 | 20 | ts := httptest.NewTLSServer( 21 | http.HandlerFunc( 22 | func(w http.ResponseWriter, _ *http.Request) { 23 | fmt.Fprintln(w, string(jsonResponse)) 24 | }, 25 | ), 26 | ) 27 | defer ts.Close() 28 | 29 | u, _ := url.Parse(ts.URL) 30 | 31 | promslogConfig := &promslog.Config{} 32 | logger := promslog.New(promslogConfig) 33 | 34 | testToken := "12345" 35 | expected := ` 36 | # HELP syncthing_rest_svc_report_hash_performance Hash Performance value 37 | # TYPE syncthing_rest_svc_report_hash_performance gauge 38 | syncthing_rest_svc_report_hash_performance 187.47 39 | # HELP syncthing_rest_svc_report_json_parse_failures Number of errors while parsing JSON. 40 | # TYPE syncthing_rest_svc_report_json_parse_failures counter 41 | syncthing_rest_svc_report_json_parse_failures 0 42 | # HELP syncthing_rest_svc_report_memory_size Node memory size in megabytes 43 | # TYPE syncthing_rest_svc_report_memory_size gauge 44 | syncthing_rest_svc_report_memory_size 8191 45 | # HELP syncthing_rest_svc_report_memory_usage_mb Memory usage by syncthc in MB 46 | # TYPE syncthing_rest_svc_report_memory_usage_mb gauge 47 | syncthing_rest_svc_report_memory_usage_mb 219 48 | # HELP syncthing_rest_svc_report_node_info Node's string information 49 | # TYPE syncthing_rest_svc_report_node_info gauge 50 | syncthing_rest_svc_report_node_info{longVersion="syncthing v1.9.0 \"Fermium Flea\" (go1.15.1 windows-amd64) teamcity@build.syncthing.net 2020-08-28 05:48:25 UTC",platform="windows-amd64",uniqueID="d9uZew76",version="v1.9.0"} 1 51 | # HELP syncthing_rest_svc_report_num_cpu Number of node CPU 52 | # TYPE syncthing_rest_svc_report_num_cpu gauge 53 | syncthing_rest_svc_report_num_cpu 4 54 | # HELP syncthing_rest_svc_report_number_devices Number of devices in sync 55 | # TYPE syncthing_rest_svc_report_number_devices gauge 56 | syncthing_rest_svc_report_number_devices 3 57 | # HELP syncthing_rest_svc_report_number_folders Number of folders in sync 58 | # TYPE syncthing_rest_svc_report_number_folders gauge 59 | syncthing_rest_svc_report_number_folders 2 60 | # HELP syncthing_rest_svc_report_sha256_performance SHA256 Performance value 61 | # TYPE syncthing_rest_svc_report_sha256_performance gauge 62 | syncthing_rest_svc_report_sha256_performance 216.93 63 | # HELP syncthing_rest_svc_report_total_data_in_MB Total data in megabytes 64 | # TYPE syncthing_rest_svc_report_total_data_in_MB gauge 65 | syncthing_rest_svc_report_total_data_in_MB 3.509532e+06 66 | # HELP syncthing_rest_svc_report_total_files Total number of files 67 | # TYPE syncthing_rest_svc_report_total_files gauge 68 | syncthing_rest_svc_report_total_files 509466 69 | # HELP syncthing_rest_svc_report_total_scrapes Current total Syncthings scrapes. 70 | # TYPE syncthing_rest_svc_report_total_scrapes counter 71 | syncthing_rest_svc_report_total_scrapes 1 72 | # HELP syncthing_rest_svc_report_up Was the last scrape of the Syncthing endpoint successful. 73 | # TYPE syncthing_rest_svc_report_up gauge 74 | syncthing_rest_svc_report_up 1 75 | # HELP syncthing_rest_svc_report_uptime Syncthing uptime in seconds 76 | # TYPE syncthing_rest_svc_report_uptime gauge 77 | syncthing_rest_svc_report_uptime 1.276967e+06 78 | ` 79 | 80 | err := testutil.CollectAndCompare( 81 | NewSVCReport(logger, HTTPClient, u, &testToken), 82 | strings.NewReader(expected), 83 | ) 84 | 85 | if err != nil { 86 | t.Errorf("NewSVCReportError %s", err) 87 | } 88 | } 89 | 90 | func TestFailedNewSVCReport(t *testing.T) { 91 | 92 | u, _ := url.Parse("http://wrong-url") 93 | promlogConfig := &promslog.Config{} 94 | logger := promslog.New(promlogConfig) 95 | 96 | testToken := "12345" 97 | expected := ` 98 | # HELP syncthing_rest_svc_report_json_parse_failures Number of errors while parsing JSON. 99 | # TYPE syncthing_rest_svc_report_json_parse_failures counter 100 | syncthing_rest_svc_report_json_parse_failures 0 101 | # HELP syncthing_rest_svc_report_total_scrapes Current total Syncthings scrapes. 102 | # TYPE syncthing_rest_svc_report_total_scrapes counter 103 | syncthing_rest_svc_report_total_scrapes 1 104 | # HELP syncthing_rest_svc_report_up Was the last scrape of the Syncthing endpoint successful. 105 | # TYPE syncthing_rest_svc_report_up gauge 106 | syncthing_rest_svc_report_up 0 107 | ` 108 | err := testutil.CollectAndCompare( 109 | NewSVCReport(logger, HTTPClient, u, &testToken), 110 | strings.NewReader(expected), 111 | ) 112 | 113 | if err != nil { 114 | t.Errorf("NewSVCReportError %s", err) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /collector/rest_system_connections.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | "net/url" 9 | 10 | "github.com/prometheus/client_golang/prometheus" 11 | ) 12 | 13 | // SCResponseBoolMetrics defines struct for boolean metrics 14 | type SCResponseBoolMetrics struct { 15 | Type prometheus.ValueType 16 | Desc *prometheus.Desc 17 | Value func(v bool) float64 18 | } 19 | 20 | // SCResponseNumericalMetrics defines struct for numeric metrics 21 | type SCResponseNumericalMetrics struct { 22 | Type prometheus.ValueType 23 | Desc *prometheus.Desc 24 | Value func(v int) float64 25 | } 26 | 27 | // SCResponseTotalNumericalMetrics defines struct for total metrics 28 | type SCResponseTotalNumericalMetrics struct { 29 | Type prometheus.ValueType 30 | Desc *prometheus.Desc 31 | Value func(v int) float64 32 | } 33 | 34 | // SCResponse defines collector struct. 35 | type SCResponse struct { 36 | logger *slog.Logger 37 | client *http.Client 38 | url *url.URL 39 | token *string 40 | 41 | up prometheus.Gauge 42 | totalScrapes, jsonParseFailures prometheus.Counter 43 | boolMetrics map[string]*SCResponseBoolMetrics 44 | numericalMetrics map[string]*SCResponseNumericalMetrics 45 | totalNumericalMetrics map[string]*SCResponseTotalNumericalMetrics 46 | } 47 | 48 | // NewSCReport returns a new Collector exposing SVCResponse 49 | func NewSCReport(logger *slog.Logger, client *http.Client, url *url.URL, token *string) *SCResponse { 50 | subsystem := "rest_system_connections" 51 | 52 | return &SCResponse{ 53 | logger: logger, 54 | client: client, 55 | url: url, 56 | token: token, 57 | 58 | up: prometheus.NewGauge(prometheus.GaugeOpts{ 59 | Name: prometheus.BuildFQName(namespace, subsystem, "up"), 60 | Help: "Was the last scrape of the Syncting system connections endpoint successful.", 61 | }), 62 | totalScrapes: prometheus.NewCounter(prometheus.CounterOpts{ 63 | Name: prometheus.BuildFQName(namespace, subsystem, "total_scrapes"), 64 | Help: "Current total Syncthings scrapes.", 65 | }), 66 | jsonParseFailures: prometheus.NewCounter(prometheus.CounterOpts{ 67 | Name: prometheus.BuildFQName(namespace, subsystem, "json_parse_failures"), 68 | Help: "Number of errors while parsing JSON.", 69 | }), 70 | boolMetrics: map[string]*SCResponseBoolMetrics{ 71 | "remote_device_connection_info": { 72 | Type: prometheus.GaugeValue, 73 | Desc: prometheus.NewDesc( 74 | prometheus.BuildFQName(namespace, subsystem, "remote_device_connection_info"), 75 | "Information of the connection with remote device. 1 - connected; 0 -disconnected.", 76 | []string{"deviceID", "clientVersion", "crypto", "type", "address"}, 77 | nil), 78 | Value: func(v bool) float64 { 79 | return bool2float64(v) 80 | }, 81 | }, 82 | "remote_device_is_paused": { 83 | Type: prometheus.GaugeValue, 84 | Desc: prometheus.NewDesc( 85 | prometheus.BuildFQName(namespace, subsystem, "remote_device_is_paused"), 86 | "Is sync paused. 1 - paused, 0 - not paused.", 87 | []string{"deviceID"}, 88 | nil), 89 | Value: func(v bool) float64 { 90 | return bool2float64(v) 91 | }, 92 | }, 93 | }, 94 | 95 | numericalMetrics: map[string]*SCResponseNumericalMetrics{ 96 | "remote_device_in_bytes_total": { 97 | Type: prometheus.GaugeValue, 98 | Desc: prometheus.NewDesc( 99 | prometheus.BuildFQName(namespace, subsystem, "remote_device_in_bytes_total"), 100 | "Total bytes received from remote device.", 101 | []string{"deviceID"}, 102 | nil), 103 | Value: func(v int) float64 { 104 | return float64(v) 105 | }, 106 | }, 107 | 108 | "remote_device_out_bytes_total": { 109 | Type: prometheus.GaugeValue, 110 | Desc: prometheus.NewDesc( 111 | prometheus.BuildFQName(namespace, subsystem, "remote_device_out_bytes_total"), 112 | "Total bytes transmitted to remote device.", 113 | []string{"deviceID"}, 114 | nil), 115 | Value: func(v int) float64 { 116 | return float64(v) 117 | }, 118 | }, 119 | }, 120 | 121 | totalNumericalMetrics: map[string]*SCResponseTotalNumericalMetrics{ 122 | "total_in_bytes_total": { 123 | Type: prometheus.GaugeValue, 124 | Desc: prometheus.NewDesc( 125 | prometheus.BuildFQName(namespace, subsystem, "total_in_bytes_total"), 126 | "Total bytes received to device.", nil, nil), 127 | Value: func(v int) float64 { 128 | return float64(v) 129 | }, 130 | }, 131 | "total_out_bytes_total": { 132 | Type: prometheus.GaugeValue, 133 | Desc: prometheus.NewDesc( 134 | prometheus.BuildFQName(namespace, subsystem, "total_out_bytes_total"), 135 | "Total bytes transmitted from device.", nil, nil), 136 | Value: func(v int) float64 { 137 | return float64(v) 138 | }, 139 | }, 140 | }, 141 | } 142 | 143 | } 144 | 145 | // Describe set Prometheus metrics descriptions. 146 | func (c *SCResponse) Describe(ch chan<- *prometheus.Desc) { 147 | 148 | for _, metric := range c.boolMetrics { 149 | ch <- metric.Desc 150 | } 151 | 152 | for _, metric := range c.numericalMetrics { 153 | ch <- metric.Desc 154 | } 155 | 156 | for _, metric := range c.totalNumericalMetrics { 157 | ch <- metric.Desc 158 | } 159 | 160 | ch <- c.up.Desc() 161 | ch <- c.totalScrapes.Desc() 162 | ch <- c.jsonParseFailures.Desc() 163 | } 164 | 165 | func (c *SCResponse) fetchDataAndDecode() (SystemConnectionsResponse, error) { 166 | var chr SystemConnectionsResponse 167 | 168 | u := *c.url 169 | url, _ := u.Parse("/rest/system/connections") 170 | 171 | h := make(http.Header) 172 | h["X-API-Key"] = []string{*c.token} 173 | 174 | request := &http.Request{ 175 | URL: url, 176 | Header: h, 177 | } 178 | 179 | res, err := c.client.Do(request) 180 | if err != nil { 181 | return chr, fmt.Errorf("failed to get data from %s://%s:%s%s: %s", 182 | u.Scheme, u.Hostname(), u.Port(), u.Path, err) 183 | } 184 | 185 | defer func() { 186 | err = res.Body.Close() 187 | if err != nil { 188 | c.logger.Warn(fmt.Sprintf("%s: %s", "failed to close http.Client", err)) 189 | } 190 | }() 191 | 192 | if res.StatusCode != http.StatusOK { 193 | return chr, fmt.Errorf("HTTP Request failed with code %d", res.StatusCode) 194 | } 195 | 196 | if err := json.NewDecoder(res.Body).Decode(&chr); err != nil { 197 | c.jsonParseFailures.Inc() 198 | return chr, err 199 | } 200 | 201 | return chr, nil 202 | 203 | } 204 | 205 | // Collect collects Syncthing metrics from /rest/system/connections. 206 | func (c *SCResponse) Collect(ch chan<- prometheus.Metric) { 207 | var err error 208 | 209 | c.totalScrapes.Inc() 210 | defer func() { 211 | ch <- c.up 212 | ch <- c.totalScrapes 213 | ch <- c.jsonParseFailures 214 | }() 215 | 216 | SCResponse, err := c.fetchDataAndDecode() 217 | if err != nil { 218 | c.up.Set(0) 219 | c.logger.Warn(fmt.Sprintf("%s: %s", "failed to fetch and decode data", err)) 220 | return 221 | } 222 | c.up.Set(1) 223 | 224 | for deviceID, deviceData := range SCResponse.CS { 225 | 226 | ch <- prometheus.MustNewConstMetric( 227 | c.boolMetrics["remote_device_connection_info"].Desc, 228 | c.boolMetrics["remote_device_connection_info"].Type, 229 | c.boolMetrics["remote_device_connection_info"].Value(deviceData.Connected), 230 | deviceID, deviceData.ClientVersion, deviceData.CryptoConfig, deviceData.Type, deviceData.IPAddress, 231 | ) 232 | ch <- prometheus.MustNewConstMetric( 233 | c.boolMetrics["remote_device_is_paused"].Desc, 234 | c.boolMetrics["remote_device_is_paused"].Type, 235 | c.boolMetrics["remote_device_is_paused"].Value(deviceData.Paused), 236 | deviceID, 237 | ) 238 | 239 | ch <- prometheus.MustNewConstMetric( 240 | c.numericalMetrics["remote_device_in_bytes_total"].Desc, 241 | c.numericalMetrics["remote_device_in_bytes_total"].Type, 242 | c.numericalMetrics["remote_device_in_bytes_total"].Value(deviceData.InBytesTotal), 243 | deviceID, 244 | ) 245 | ch <- prometheus.MustNewConstMetric( 246 | c.numericalMetrics["remote_device_out_bytes_total"].Desc, 247 | c.numericalMetrics["remote_device_out_bytes_total"].Type, 248 | c.numericalMetrics["remote_device_out_bytes_total"].Value(deviceData.OutBytesTotal), 249 | deviceID, 250 | ) 251 | 252 | } 253 | 254 | ch <- prometheus.MustNewConstMetric( 255 | c.totalNumericalMetrics["total_in_bytes_total"].Desc, 256 | c.totalNumericalMetrics["total_in_bytes_total"].Type, 257 | c.totalNumericalMetrics["total_in_bytes_total"].Value(SCResponse.Total.InBytesTotal), 258 | ) 259 | ch <- prometheus.MustNewConstMetric( 260 | c.totalNumericalMetrics["total_out_bytes_total"].Desc, 261 | c.totalNumericalMetrics["total_out_bytes_total"].Type, 262 | c.totalNumericalMetrics["total_out_bytes_total"].Value(SCResponse.Total.OutBytesTotal), 263 | ) 264 | } 265 | -------------------------------------------------------------------------------- /collector/rest_system_connections_response.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | type connectionParameters struct { 4 | IPAddress string `json:"address"` 5 | AtDate string `json:"at"` 6 | ClientVersion string `json:"clientVersion"` 7 | Connected bool `json:"connected"` 8 | CryptoConfig string `json:"crypto"` 9 | InBytesTotal int `json:"inBytesTotal"` 10 | OutBytesTotal int `json:"outBytesTotal"` 11 | Paused bool `json:"paused"` 12 | Type string `json:"type"` 13 | } 14 | 15 | // SystemConnectionsResponse defines response struct 16 | // for /rest/system/connections endpoint. 17 | // Tested with Syncthing version 1.9.0. 18 | type SystemConnectionsResponse struct { 19 | CS map[string]connectionParameters `json:"connections"` 20 | Total connectionParameters `json:"total"` 21 | } 22 | -------------------------------------------------------------------------------- /collector/rest_system_connections_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "os" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/prometheus/client_golang/prometheus/testutil" 13 | "github.com/prometheus/common/promslog" 14 | ) 15 | 16 | func TestNewSCReport(t *testing.T) { 17 | 18 | jsonResponse, err := os.ReadFile("fixtures/rest_system_connections_response.json") 19 | if err != nil { 20 | t.Error("Cant open fixtures file") 21 | } 22 | 23 | ts := httptest.NewTLSServer( 24 | http.HandlerFunc( 25 | func(w http.ResponseWriter, _ *http.Request) { 26 | fmt.Fprintln(w, string(jsonResponse)) 27 | }, 28 | ), 29 | ) 30 | defer ts.Close() 31 | 32 | u, err := url.Parse(ts.URL) 33 | if err != nil { 34 | t.Errorf("url parse error: %s", err) 35 | } 36 | 37 | promslogConfig := &promslog.Config{} 38 | logger := promslog.New(promslogConfig) 39 | 40 | testToken := "12345" 41 | expected := ` 42 | # HELP syncthing_rest_system_connections_json_parse_failures Number of errors while parsing JSON. 43 | # TYPE syncthing_rest_system_connections_json_parse_failures counter 44 | syncthing_rest_system_connections_json_parse_failures 0 45 | # HELP syncthing_rest_system_connections_remote_device_connection_info Information of the connection with remote device. 1 - connected; 0 -disconnected. 46 | # TYPE syncthing_rest_system_connections_remote_device_connection_info gauge 47 | syncthing_rest_system_connections_remote_device_connection_info{address="",clientVersion="",crypto="",deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC",type=""} 0 48 | syncthing_rest_system_connections_remote_device_connection_info{address="10.1.0.1:59172",clientVersion="v1.10.0",crypto="TLS1.3-TLS_AES_128_GCM_SHA256",deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA",type="tcp-server"} 1 49 | syncthing_rest_system_connections_remote_device_connection_info{address="10.2.0.2:22000",clientVersion="v1.10.0",crypto="TLS1.3-TLS_CHACHA20_POLY1305_SHA256",deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB",type="tcp-client"} 1 50 | # HELP syncthing_rest_system_connections_remote_device_in_bytes_total Total bytes received from remote device. 51 | # TYPE syncthing_rest_system_connections_remote_device_in_bytes_total gauge 52 | syncthing_rest_system_connections_remote_device_in_bytes_total{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA"} 1.119463078e+09 53 | syncthing_rest_system_connections_remote_device_in_bytes_total{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB"} 1.0431439e+07 54 | syncthing_rest_system_connections_remote_device_in_bytes_total{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC"} 0 55 | # HELP syncthing_rest_system_connections_remote_device_is_paused Is sync paused. 1 - paused, 0 - not paused. 56 | # TYPE syncthing_rest_system_connections_remote_device_is_paused gauge 57 | syncthing_rest_system_connections_remote_device_is_paused{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA"} 0 58 | syncthing_rest_system_connections_remote_device_is_paused{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB"} 0 59 | syncthing_rest_system_connections_remote_device_is_paused{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC"} 0 60 | # HELP syncthing_rest_system_connections_remote_device_out_bytes_total Total bytes transmitted to remote device. 61 | # TYPE syncthing_rest_system_connections_remote_device_out_bytes_total gauge 62 | syncthing_rest_system_connections_remote_device_out_bytes_total{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA"} 1.636080926e+10 63 | syncthing_rest_system_connections_remote_device_out_bytes_total{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB"} 6.072477885e+09 64 | syncthing_rest_system_connections_remote_device_out_bytes_total{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC"} 0 65 | # HELP syncthing_rest_system_connections_total_in_bytes_total Total bytes received to device. 66 | # TYPE syncthing_rest_system_connections_total_in_bytes_total gauge 67 | syncthing_rest_system_connections_total_in_bytes_total 2.458124935e+09 68 | # HELP syncthing_rest_system_connections_total_out_bytes_total Total bytes transmitted from device. 69 | # TYPE syncthing_rest_system_connections_total_out_bytes_total gauge 70 | syncthing_rest_system_connections_total_out_bytes_total 3.3180203458e+10 71 | # HELP syncthing_rest_system_connections_total_scrapes Current total Syncthings scrapes. 72 | # TYPE syncthing_rest_system_connections_total_scrapes counter 73 | syncthing_rest_system_connections_total_scrapes 1 74 | # HELP syncthing_rest_system_connections_up Was the last scrape of the Syncting system connections endpoint successful. 75 | # TYPE syncthing_rest_system_connections_up gauge 76 | syncthing_rest_system_connections_up 1 77 | ` 78 | err = testutil.CollectAndCompare( 79 | NewSCReport(logger, HTTPClient, u, &testToken), 80 | strings.NewReader(expected), 81 | ) 82 | 83 | if err != nil { 84 | t.Errorf("NewSCReportError %s", err) 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /examples/exposed_parameters.md: -------------------------------------------------------------------------------- 1 | 2 | # Exposing metrics 3 | 4 | [back to README.md](../README.md) 5 | 6 | ``` 7 | # HELP syncthing_exporter_build_info A metric with a constant '1' value labeled by version, revision, branch, and goversion from which syncthing_exporter was built. 8 | # TYPE syncthing_exporter_build_info gauge 9 | syncthing_exporter_build_info{branch="",goversion="go1.16",revision="",version=""} 1 10 | # HELP syncthing_rest_db_status_errors Number of errors for current folder. 11 | # TYPE syncthing_rest_db_status_errors gauge 12 | syncthing_rest_db_status_errors{folderID="aaaaa-bb11b"} 0 13 | # HELP syncthing_rest_db_status_global_bytes Number of bytes globally. 14 | # TYPE syncthing_rest_db_status_global_bytes gauge 15 | syncthing_rest_db_status_global_bytes{folderID="aaaaa-bb11b"} 4.047955455069e+12 16 | # HELP syncthing_rest_db_status_global_deleted Number of bytes deleted. 17 | # TYPE syncthing_rest_db_status_global_deleted gauge 18 | syncthing_rest_db_status_global_deleted{folderID="aaaaa-bb11b"} 156112 19 | # HELP syncthing_rest_db_status_global_directories Number of directories globally. 20 | # TYPE syncthing_rest_db_status_global_directories gauge 21 | syncthing_rest_db_status_global_directories{folderID="aaaaa-bb11b"} 346372 22 | # HELP syncthing_rest_db_status_global_symlinks Number of symlinks globally. 23 | # TYPE syncthing_rest_db_status_global_symlinks gauge 24 | syncthing_rest_db_status_global_symlinks{folderID="aaaaa-bb11b"} 0 25 | # HELP syncthing_rest_db_status_global_total_items Number of total items globally. 26 | # TYPE syncthing_rest_db_status_global_total_items gauge 27 | syncthing_rest_db_status_global_total_items{folderID="aaaaa-bb11b"} 1.197108e+06 28 | # HELP syncthing_rest_db_status_ignore_patterns Is using ignore patterns. 29 | # TYPE syncthing_rest_db_status_ignore_patterns gauge 30 | syncthing_rest_db_status_ignore_patterns{folderID="aaaaa-bb11b"} 0 31 | # HELP syncthing_rest_db_status_insync_bytes Number of bytes currently in sync. 32 | # TYPE syncthing_rest_db_status_insync_bytes gauge 33 | syncthing_rest_db_status_insync_bytes{folderID="aaaaa-bb11b"} 4.047955455069e+12 34 | # HELP syncthing_rest_db_status_insync_files Number of files currently in sync. 35 | # TYPE syncthing_rest_db_status_insync_files gauge 36 | syncthing_rest_db_status_insync_files{folderID="aaaaa-bb11b"} 694624 37 | # HELP syncthing_rest_db_status_json_parse_failures Number of errors while parsing JSON. 38 | # TYPE syncthing_rest_db_status_json_parse_failures counter 39 | syncthing_rest_db_status_json_parse_failures 0 40 | # HELP syncthing_rest_db_status_local_bytes Number of bytes locally. 41 | # TYPE syncthing_rest_db_status_local_bytes gauge 42 | syncthing_rest_db_status_local_bytes{folderID="aaaaa-bb11b"} 4.047955455069e+12 43 | # HELP syncthing_rest_db_status_local_deleted Number of bytes deleted locally. 44 | # TYPE syncthing_rest_db_status_local_deleted gauge 45 | syncthing_rest_db_status_local_deleted{folderID="aaaaa-bb11b"} 318 46 | # HELP syncthing_rest_db_status_local_directories Number of local directories. 47 | # TYPE syncthing_rest_db_status_local_directories gauge 48 | syncthing_rest_db_status_local_directories{folderID="aaaaa-bb11b"} 346372 49 | # HELP syncthing_rest_db_status_local_symlinks Number of local symlinks 50 | # TYPE syncthing_rest_db_status_local_symlinks gauge 51 | syncthing_rest_db_status_local_symlinks{folderID="aaaaa-bb11b"} 0 52 | # HELP syncthing_rest_db_status_local_total_items Number of total items locally 53 | # TYPE syncthing_rest_db_status_local_total_items gauge 54 | syncthing_rest_db_status_local_total_items{folderID="aaaaa-bb11b"} 1.041314e+06 55 | # HELP syncthing_rest_db_status_need_bytes Number of bytes need for sync. 56 | # TYPE syncthing_rest_db_status_need_bytes gauge 57 | syncthing_rest_db_status_need_bytes{folderID="aaaaa-bb11b"} 0 58 | # HELP syncthing_rest_db_status_need_deletes Number of bytes need for deletes. 59 | # TYPE syncthing_rest_db_status_need_deletes gauge 60 | syncthing_rest_db_status_need_deletes{folderID="aaaaa-bb11b"} 0 61 | # HELP syncthing_rest_db_status_need_directories Number of directories for sync. 62 | # TYPE syncthing_rest_db_status_need_directories gauge 63 | syncthing_rest_db_status_need_directories{folderID="aaaaa-bb11b"} 0 64 | # HELP syncthing_rest_db_status_need_symlinks Number of symlinks need for sync. 65 | # TYPE syncthing_rest_db_status_need_symlinks gauge 66 | syncthing_rest_db_status_need_symlinks{folderID="aaaaa-bb11b"} 0 67 | # HELP syncthing_rest_db_status_need_total_items Number of total items need to sync. 68 | # TYPE syncthing_rest_db_status_need_total_items gauge 69 | syncthing_rest_db_status_need_total_items{folderID="aaaaa-bb11b"} 0 70 | # HELP syncthing_rest_db_status_pull_errors Number of pull errors. 71 | # TYPE syncthing_rest_db_status_pull_errors gauge 72 | syncthing_rest_db_status_pull_errors{folderID="aaaaa-bb11b"} 0 73 | # HELP syncthing_rest_db_status_sequence Total bytes received from remote device. 74 | # TYPE syncthing_rest_db_status_sequence gauge 75 | syncthing_rest_db_status_sequence{folderID="aaaaa-bb11b",state="idle",stateChanged="2077-02-06T00:02:00-07:00"} 1.213411e+06 76 | # HELP syncthing_rest_db_status_total_scrapes Current total Syncthings scrapes. 77 | # TYPE syncthing_rest_db_status_total_scrapes counter 78 | syncthing_rest_db_status_total_scrapes 1 79 | # HELP syncthing_rest_stats_device_json_parse_failures Number of errors while parsing JSON. 80 | # TYPE syncthing_rest_stats_device_json_parse_failures counter 81 | syncthing_rest_stats_device_json_parse_failures 0 82 | # HELP syncthing_rest_stats_device_last_connection_duration Duration of last connection with remote device in seconds. 83 | # TYPE syncthing_rest_stats_device_last_connection_duration gauge 84 | syncthing_rest_stats_device_last_connection_duration{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA",lastSeen="2077-12-31T00:00:50.3810375-08:00"} 0 85 | syncthing_rest_stats_device_last_connection_duration{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB",lastSeen="2077-04-14T22:14:49-07:00"} 42832.8784021 86 | syncthing_rest_stats_device_last_connection_duration{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC",lastSeen="1977-12-31T16:00:00-08:00"} 0 87 | # HELP syncthing_rest_stats_device_total_scrapes Current total Syncthings scrapes. 88 | # TYPE syncthing_rest_stats_device_total_scrapes counter 89 | syncthing_rest_stats_device_total_scrapes 1 90 | # HELP syncthing_rest_stats_device_up Was the last scrape of the Syncting system connections endpoint successful. 91 | # TYPE syncthing_rest_stats_device_up gauge 92 | syncthing_rest_stats_device_up 1 93 | # HELP syncthing_rest_svc_report_hash_performance Hash Performance value 94 | # TYPE syncthing_rest_svc_report_hash_performance gauge 95 | syncthing_rest_svc_report_hash_performance 104.92 96 | # HELP syncthing_rest_svc_report_json_parse_failures Number of errors while parsing JSON. 97 | # TYPE syncthing_rest_svc_report_json_parse_failures counter 98 | syncthing_rest_svc_report_json_parse_failures 0 99 | # HELP syncthing_rest_svc_report_memory_size Node memory size in megabytes 100 | # TYPE syncthing_rest_svc_report_memory_size gauge 101 | syncthing_rest_svc_report_memory_size 8191 102 | # HELP syncthing_rest_svc_report_memory_usage_mb Memory usage by syncthc in MB 103 | # TYPE syncthing_rest_svc_report_memory_usage_mb gauge 104 | syncthing_rest_svc_report_memory_usage_mb 264 105 | # HELP syncthing_rest_svc_report_node_info Node's string information 106 | # TYPE syncthing_rest_svc_report_node_info gauge 107 | syncthing_rest_svc_report_node_info{longVersion="syncthing v1.15.1 \"Fermium Flea\" (go1.16.3 windows-amd64) teamcity@build.syncthing.net 2021-04-06 08:42:29 UTC",platform="windows-amd64",uniqueID="d9uZew76",version="v1.15.1"} 1 108 | # HELP syncthing_rest_svc_report_num_cpu Number of node CPU 109 | # TYPE syncthing_rest_svc_report_num_cpu gauge 110 | syncthing_rest_svc_report_num_cpu 4 111 | # HELP syncthing_rest_svc_report_number_devices Number of devices in sync 112 | # TYPE syncthing_rest_svc_report_number_devices gauge 113 | syncthing_rest_svc_report_number_devices 3 114 | # HELP syncthing_rest_svc_report_number_folders Number of folders in sync 115 | # TYPE syncthing_rest_svc_report_number_folders gauge 116 | syncthing_rest_svc_report_number_folders 2 117 | # HELP syncthing_rest_svc_report_sha256_performance SHA256 Performance value 118 | # TYPE syncthing_rest_svc_report_sha256_performance gauge 119 | syncthing_rest_svc_report_sha256_performance 119.87 120 | # HELP syncthing_rest_svc_report_total_data_in_MB Total data in megabytes 121 | # TYPE syncthing_rest_svc_report_total_data_in_MB gauge 122 | syncthing_rest_svc_report_total_data_in_MB 3.860566e+06 123 | # HELP syncthing_rest_svc_report_total_files Total number of files 124 | # TYPE syncthing_rest_svc_report_total_files gauge 125 | syncthing_rest_svc_report_total_files 725584 126 | # HELP syncthing_rest_svc_report_total_scrapes Current total Syncthings scrapes. 127 | # TYPE syncthing_rest_svc_report_total_scrapes counter 128 | syncthing_rest_svc_report_total_scrapes 1 129 | # HELP syncthing_rest_svc_report_up Was the last scrape of the Syncthing endpoint successful. 130 | # TYPE syncthing_rest_svc_report_up gauge 131 | syncthing_rest_svc_report_up 1 132 | # HELP syncthing_rest_svc_report_uptime Syncthing uptime in seconds 133 | # TYPE syncthing_rest_svc_report_uptime gauge 134 | syncthing_rest_svc_report_uptime 51271 135 | # HELP syncthing_rest_system_connections_json_parse_failures Number of errors while parsing JSON. 136 | # TYPE syncthing_rest_system_connections_json_parse_failures counter 137 | syncthing_rest_system_connections_json_parse_failures 0 138 | # HELP syncthing_rest_system_connections_remote_device_connection_info Information of the connection with remote device. 1 - connected; 0 -disconnected. 139 | # TYPE syncthing_rest_system_connections_remote_device_connection_info gauge 140 | syncthing_rest_system_connections_remote_device_connection_info{address="",clientVersion="",crypto="",deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA",type=""} 0 141 | syncthing_rest_system_connections_remote_device_connection_info{address="",clientVersion="",crypto="",deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB",type=""} 0 142 | syncthing_rest_system_connections_remote_device_connection_info{address="127.0.0.1:1234",clientVersion="v1.15.1",crypto="TLS1.3-TLS_CHACHA20_POLY1305_SHA256",deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC",type="tcp-client"} 1 143 | # HELP syncthing_rest_system_connections_remote_device_in_bytes_total Total bytes received from remote device. 144 | # TYPE syncthing_rest_system_connections_remote_device_in_bytes_total gauge 145 | syncthing_rest_system_connections_remote_device_in_bytes_total{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA"} 0 146 | syncthing_rest_system_connections_remote_device_in_bytes_total{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB"} 1.199599e+06 147 | syncthing_rest_system_connections_remote_device_in_bytes_total{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC"} 0 148 | # HELP syncthing_rest_system_connections_remote_device_is_paused Is sync paused. 1 - paused, 0 - not paused. 149 | # TYPE syncthing_rest_system_connections_remote_device_is_paused gauge 150 | syncthing_rest_system_connections_remote_device_is_paused{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA"} 1 151 | syncthing_rest_system_connections_remote_device_is_paused{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB"} 0 152 | syncthing_rest_system_connections_remote_device_is_paused{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC"} 0 153 | # HELP syncthing_rest_system_connections_remote_device_out_bytes_total Total bytes transmitted to remote device. 154 | # TYPE syncthing_rest_system_connections_remote_device_out_bytes_total gauge 155 | syncthing_rest_system_connections_remote_device_out_bytes_total{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA"} 0 156 | syncthing_rest_system_connections_remote_device_out_bytes_total{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-BBBBBBB"} 7.01416862e+08 157 | syncthing_rest_system_connections_remote_device_out_bytes_total{deviceID="AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-AAAAAAA-CCCCCCC"} 0 158 | # HELP syncthing_rest_system_connections_total_in_bytes_total Total bytes received to device. 159 | # TYPE syncthing_rest_system_connections_total_in_bytes_total gauge 160 | syncthing_rest_system_connections_total_in_bytes_total 1.199599e+06 161 | # HELP syncthing_rest_system_connections_total_out_bytes_total Total bytes transmitted from device. 162 | # TYPE syncthing_rest_system_connections_total_out_bytes_total gauge 163 | syncthing_rest_system_connections_total_out_bytes_total 7.01416862e+08 164 | # HELP syncthing_rest_system_connections_total_scrapes Current total Syncthings scrapes. 165 | # TYPE syncthing_rest_system_connections_total_scrapes counter 166 | syncthing_rest_system_connections_total_scrapes 1 167 | # HELP syncthing_rest_system_connections_up Was the last scrape of the Syncting system connections endpoint successful. 168 | # TYPE syncthing_rest_system_connections_up gauge 169 | syncthing_rest_system_connections_up 1 170 | ``` 171 | -------------------------------------------------------------------------------- /examples/grafana/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "Prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__elements": {}, 13 | "__requires": [ 14 | { 15 | "type": "grafana", 16 | "id": "grafana", 17 | "name": "Grafana", 18 | "version": "10.2.0" 19 | }, 20 | { 21 | "type": "panel", 22 | "id": "graph", 23 | "name": "Graph (old)", 24 | "version": "" 25 | }, 26 | { 27 | "type": "datasource", 28 | "id": "prometheus", 29 | "name": "Prometheus", 30 | "version": "1.0.0" 31 | }, 32 | { 33 | "type": "panel", 34 | "id": "stat", 35 | "name": "Stat", 36 | "version": "" 37 | } 38 | ], 39 | "annotations": { 40 | "list": [ 41 | { 42 | "builtIn": 1, 43 | "datasource": { 44 | "type": "datasource", 45 | "uid": "grafana" 46 | }, 47 | "enable": true, 48 | "hide": true, 49 | "iconColor": "rgba(0, 211, 255, 1)", 50 | "name": "Annotations & Alerts", 51 | "type": "dashboard" 52 | } 53 | ] 54 | }, 55 | "editable": true, 56 | "fiscalYearStartMonth": 0, 57 | "graphTooltip": 0, 58 | "id": null, 59 | "links": [], 60 | "liveNow": false, 61 | "panels": [ 62 | { 63 | "datasource": { 64 | "type": "prometheus", 65 | "uid": "${DS_PROMETHEUS}" 66 | }, 67 | "fieldConfig": { 68 | "defaults": { 69 | "color": { 70 | "mode": "thresholds" 71 | }, 72 | "mappings": [ 73 | { 74 | "options": { 75 | "0": { 76 | "text": "Failed" 77 | }, 78 | "1": { 79 | "text": "OK" 80 | } 81 | }, 82 | "type": "value" 83 | }, 84 | { 85 | "options": { 86 | "match": "null", 87 | "result": { 88 | "text": "No data" 89 | } 90 | }, 91 | "type": "special" 92 | } 93 | ], 94 | "thresholds": { 95 | "mode": "absolute", 96 | "steps": [ 97 | { 98 | "color": "dark-purple", 99 | "value": null 100 | }, 101 | { 102 | "color": "red", 103 | "value": 0 104 | }, 105 | { 106 | "color": "green", 107 | "value": 1 108 | } 109 | ] 110 | }, 111 | "unit": "none" 112 | }, 113 | "overrides": [] 114 | }, 115 | "gridPos": { 116 | "h": 5, 117 | "w": 3, 118 | "x": 0, 119 | "y": 0 120 | }, 121 | "id": 32, 122 | "links": [], 123 | "maxDataPoints": 100, 124 | "options": { 125 | "colorMode": "background", 126 | "graphMode": "none", 127 | "justifyMode": "auto", 128 | "orientation": "horizontal", 129 | "reduceOptions": { 130 | "calcs": [ 131 | "lastNotNull" 132 | ], 133 | "fields": "", 134 | "values": false 135 | }, 136 | "textMode": "auto" 137 | }, 138 | "pluginVersion": "10.2.0", 139 | "targets": [ 140 | { 141 | "datasource": { 142 | "type": "prometheus", 143 | "uid": "${DS_PROMETHEUS}" 144 | }, 145 | "editorMode": "code", 146 | "expr": "syncthing_rest_svc_report_up", 147 | "range": true, 148 | "refId": "A" 149 | } 150 | ], 151 | "title": "Node status", 152 | "type": "stat" 153 | }, 154 | { 155 | "datasource": { 156 | "type": "prometheus", 157 | "uid": "${DS_PROMETHEUS}" 158 | }, 159 | "fieldConfig": { 160 | "defaults": { 161 | "color": { 162 | "mode": "thresholds" 163 | }, 164 | "mappings": [ 165 | { 166 | "options": { 167 | "match": "null", 168 | "result": { 169 | "text": "No data" 170 | } 171 | }, 172 | "type": "special" 173 | } 174 | ], 175 | "thresholds": { 176 | "mode": "absolute", 177 | "steps": [ 178 | { 179 | "color": "green", 180 | "value": null 181 | }, 182 | { 183 | "color": "red", 184 | "value": 80 185 | } 186 | ] 187 | }, 188 | "unit": "decmbytes" 189 | }, 190 | "overrides": [] 191 | }, 192 | "gridPos": { 193 | "h": 5, 194 | "w": 3, 195 | "x": 3, 196 | "y": 0 197 | }, 198 | "id": 30, 199 | "links": [], 200 | "maxDataPoints": 100, 201 | "options": { 202 | "colorMode": "none", 203 | "graphMode": "none", 204 | "justifyMode": "auto", 205 | "orientation": "horizontal", 206 | "reduceOptions": { 207 | "calcs": [ 208 | "lastNotNull" 209 | ], 210 | "fields": "", 211 | "values": false 212 | }, 213 | "textMode": "auto" 214 | }, 215 | "pluginVersion": "10.2.0", 216 | "targets": [ 217 | { 218 | "datasource": { 219 | "type": "prometheus", 220 | "uid": "${DS_PROMETHEUS}" 221 | }, 222 | "expr": "syncthing_rest_svc_report_memory_size", 223 | "refId": "A" 224 | } 225 | ], 226 | "title": "Node memory size in megabytes", 227 | "type": "stat" 228 | }, 229 | { 230 | "datasource": { 231 | "type": "prometheus", 232 | "uid": "${DS_PROMETHEUS}" 233 | }, 234 | "fieldConfig": { 235 | "defaults": { 236 | "color": { 237 | "mode": "thresholds" 238 | }, 239 | "mappings": [ 240 | { 241 | "options": { 242 | "match": "null", 243 | "result": { 244 | "text": "N/A" 245 | } 246 | }, 247 | "type": "special" 248 | } 249 | ], 250 | "thresholds": { 251 | "mode": "absolute", 252 | "steps": [ 253 | { 254 | "color": "green", 255 | "value": null 256 | }, 257 | { 258 | "color": "red", 259 | "value": 80 260 | } 261 | ] 262 | }, 263 | "unit": "bytes" 264 | }, 265 | "overrides": [] 266 | }, 267 | "gridPos": { 268 | "h": 5, 269 | "w": 4, 270 | "x": 6, 271 | "y": 0 272 | }, 273 | "id": 24, 274 | "links": [], 275 | "maxDataPoints": 100, 276 | "options": { 277 | "colorMode": "none", 278 | "graphMode": "none", 279 | "justifyMode": "auto", 280 | "orientation": "horizontal", 281 | "reduceOptions": { 282 | "calcs": [ 283 | "mean" 284 | ], 285 | "fields": "", 286 | "values": false 287 | }, 288 | "textMode": "auto" 289 | }, 290 | "pluginVersion": "10.2.0", 291 | "targets": [ 292 | { 293 | "datasource": { 294 | "type": "prometheus", 295 | "uid": "${DS_PROMETHEUS}" 296 | }, 297 | "expr": "syncthing_rest_system_connections_total_out_bytes_total", 298 | "refId": "A" 299 | } 300 | ], 301 | "title": "Total bytes transmitted from device", 302 | "type": "stat" 303 | }, 304 | { 305 | "datasource": { 306 | "type": "prometheus", 307 | "uid": "${DS_PROMETHEUS}" 308 | }, 309 | "fieldConfig": { 310 | "defaults": { 311 | "color": { 312 | "mode": "thresholds" 313 | }, 314 | "mappings": [ 315 | { 316 | "options": { 317 | "match": "null", 318 | "result": { 319 | "text": "N/A" 320 | } 321 | }, 322 | "type": "special" 323 | } 324 | ], 325 | "thresholds": { 326 | "mode": "absolute", 327 | "steps": [ 328 | { 329 | "color": "green", 330 | "value": null 331 | }, 332 | { 333 | "color": "red", 334 | "value": 80 335 | } 336 | ] 337 | }, 338 | "unit": "decmbytes" 339 | }, 340 | "overrides": [] 341 | }, 342 | "gridPos": { 343 | "h": 5, 344 | "w": 3, 345 | "x": 10, 346 | "y": 0 347 | }, 348 | "id": 4, 349 | "links": [], 350 | "maxDataPoints": 100, 351 | "options": { 352 | "colorMode": "none", 353 | "graphMode": "none", 354 | "justifyMode": "auto", 355 | "orientation": "horizontal", 356 | "reduceOptions": { 357 | "calcs": [ 358 | "lastNotNull" 359 | ], 360 | "fields": "", 361 | "values": false 362 | }, 363 | "textMode": "auto" 364 | }, 365 | "pluginVersion": "10.2.0", 366 | "targets": [ 367 | { 368 | "datasource": { 369 | "type": "prometheus", 370 | "uid": "${DS_PROMETHEUS}" 371 | }, 372 | "expr": "syncthing_rest_svc_report_total_data_in_MB", 373 | "format": "time_series", 374 | "refId": "A" 375 | } 376 | ], 377 | "title": "Total data in megabytes", 378 | "type": "stat" 379 | }, 380 | { 381 | "datasource": { 382 | "type": "prometheus", 383 | "uid": "${DS_PROMETHEUS}" 384 | }, 385 | "fieldConfig": { 386 | "defaults": { 387 | "color": { 388 | "mode": "thresholds" 389 | }, 390 | "mappings": [ 391 | { 392 | "options": { 393 | "match": "null", 394 | "result": { 395 | "text": "N/A" 396 | } 397 | }, 398 | "type": "special" 399 | } 400 | ], 401 | "thresholds": { 402 | "mode": "absolute", 403 | "steps": [ 404 | { 405 | "color": "green", 406 | "value": null 407 | }, 408 | { 409 | "color": "red", 410 | "value": 80 411 | } 412 | ] 413 | }, 414 | "unit": "bytes" 415 | }, 416 | "overrides": [] 417 | }, 418 | "gridPos": { 419 | "h": 5, 420 | "w": 3, 421 | "x": 13, 422 | "y": 0 423 | }, 424 | "id": 22, 425 | "links": [], 426 | "maxDataPoints": 100, 427 | "options": { 428 | "colorMode": "none", 429 | "graphMode": "none", 430 | "justifyMode": "auto", 431 | "orientation": "horizontal", 432 | "reduceOptions": { 433 | "calcs": [ 434 | "mean" 435 | ], 436 | "fields": "", 437 | "values": false 438 | }, 439 | "textMode": "auto" 440 | }, 441 | "pluginVersion": "10.2.0", 442 | "targets": [ 443 | { 444 | "datasource": { 445 | "type": "prometheus", 446 | "uid": "${DS_PROMETHEUS}" 447 | }, 448 | "expr": "syncthing_rest_system_connections_total_in_bytes_total", 449 | "refId": "A" 450 | } 451 | ], 452 | "title": "Total bytes received to device", 453 | "type": "stat" 454 | }, 455 | { 456 | "datasource": { 457 | "type": "prometheus", 458 | "uid": "${DS_PROMETHEUS}" 459 | }, 460 | "fieldConfig": { 461 | "defaults": { 462 | "color": { 463 | "mode": "thresholds" 464 | }, 465 | "mappings": [ 466 | { 467 | "options": { 468 | "match": "null", 469 | "result": { 470 | "text": "N/A" 471 | } 472 | }, 473 | "type": "special" 474 | } 475 | ], 476 | "thresholds": { 477 | "mode": "absolute", 478 | "steps": [ 479 | { 480 | "color": "green", 481 | "value": null 482 | }, 483 | { 484 | "color": "red", 485 | "value": 80 486 | } 487 | ] 488 | }, 489 | "unit": "none" 490 | }, 491 | "overrides": [] 492 | }, 493 | "gridPos": { 494 | "h": 5, 495 | "w": 3, 496 | "x": 16, 497 | "y": 0 498 | }, 499 | "id": 28, 500 | "links": [], 501 | "maxDataPoints": 100, 502 | "options": { 503 | "colorMode": "none", 504 | "graphMode": "none", 505 | "justifyMode": "auto", 506 | "orientation": "horizontal", 507 | "reduceOptions": { 508 | "calcs": [ 509 | "lastNotNull" 510 | ], 511 | "fields": "", 512 | "values": false 513 | }, 514 | "textMode": "auto" 515 | }, 516 | "pluginVersion": "10.2.0", 517 | "targets": [ 518 | { 519 | "datasource": { 520 | "type": "prometheus", 521 | "uid": "${DS_PROMETHEUS}" 522 | }, 523 | "expr": "syncthing_rest_svc_report_num_cpu", 524 | "refId": "A" 525 | } 526 | ], 527 | "title": "Number of node CPU", 528 | "type": "stat" 529 | }, 530 | { 531 | "datasource": { 532 | "type": "prometheus", 533 | "uid": "${DS_PROMETHEUS}" 534 | }, 535 | "fieldConfig": { 536 | "defaults": { 537 | "color": { 538 | "mode": "thresholds" 539 | }, 540 | "mappings": [ 541 | { 542 | "options": { 543 | "match": "null", 544 | "result": { 545 | "text": "N/A" 546 | } 547 | }, 548 | "type": "special" 549 | } 550 | ], 551 | "thresholds": { 552 | "mode": "absolute", 553 | "steps": [ 554 | { 555 | "color": "green", 556 | "value": null 557 | }, 558 | { 559 | "color": "red", 560 | "value": 80 561 | } 562 | ] 563 | }, 564 | "unit": "s" 565 | }, 566 | "overrides": [] 567 | }, 568 | "gridPos": { 569 | "h": 5, 570 | "w": 5, 571 | "x": 19, 572 | "y": 0 573 | }, 574 | "id": 8, 575 | "links": [], 576 | "maxDataPoints": 100, 577 | "options": { 578 | "colorMode": "none", 579 | "graphMode": "none", 580 | "justifyMode": "auto", 581 | "orientation": "horizontal", 582 | "reduceOptions": { 583 | "calcs": [ 584 | "lastNotNull" 585 | ], 586 | "fields": "", 587 | "values": false 588 | }, 589 | "textMode": "auto" 590 | }, 591 | "pluginVersion": "10.2.0", 592 | "targets": [ 593 | { 594 | "datasource": { 595 | "type": "prometheus", 596 | "uid": "${DS_PROMETHEUS}" 597 | }, 598 | "expr": "syncthing_rest_svc_report_uptime", 599 | "refId": "A" 600 | } 601 | ], 602 | "title": "Syncthing uptime in seconds", 603 | "type": "stat" 604 | }, 605 | { 606 | "datasource": { 607 | "type": "prometheus", 608 | "uid": "${DS_PROMETHEUS}" 609 | }, 610 | "fieldConfig": { 611 | "defaults": { 612 | "color": { 613 | "mode": "thresholds" 614 | }, 615 | "mappings": [ 616 | { 617 | "options": { 618 | "match": "null", 619 | "result": { 620 | "text": "N/A" 621 | } 622 | }, 623 | "type": "special" 624 | } 625 | ], 626 | "thresholds": { 627 | "mode": "absolute", 628 | "steps": [ 629 | { 630 | "color": "green", 631 | "value": null 632 | }, 633 | { 634 | "color": "red", 635 | "value": 80 636 | } 637 | ] 638 | }, 639 | "unit": "none" 640 | }, 641 | "overrides": [] 642 | }, 643 | "gridPos": { 644 | "h": 5, 645 | "w": 3, 646 | "x": 0, 647 | "y": 5 648 | }, 649 | "id": 6, 650 | "links": [], 651 | "maxDataPoints": 100, 652 | "options": { 653 | "colorMode": "none", 654 | "graphMode": "none", 655 | "justifyMode": "auto", 656 | "orientation": "horizontal", 657 | "reduceOptions": { 658 | "calcs": [ 659 | "lastNotNull" 660 | ], 661 | "fields": "", 662 | "values": false 663 | }, 664 | "textMode": "auto" 665 | }, 666 | "pluginVersion": "10.2.0", 667 | "targets": [ 668 | { 669 | "datasource": { 670 | "type": "prometheus", 671 | "uid": "${DS_PROMETHEUS}" 672 | }, 673 | "expr": "syncthing_rest_svc_report_total_files", 674 | "refId": "A" 675 | } 676 | ], 677 | "title": "Total number of files", 678 | "type": "stat" 679 | }, 680 | { 681 | "datasource": { 682 | "type": "prometheus", 683 | "uid": "${DS_PROMETHEUS}" 684 | }, 685 | "fieldConfig": { 686 | "defaults": { 687 | "color": { 688 | "mode": "thresholds" 689 | }, 690 | "mappings": [ 691 | { 692 | "options": { 693 | "0": { 694 | "text": "Failed" 695 | }, 696 | "1": { 697 | "text": "$myClientLongVersion" 698 | } 699 | }, 700 | "type": "value" 701 | }, 702 | { 703 | "options": { 704 | "match": "null", 705 | "result": { 706 | "text": "No data" 707 | } 708 | }, 709 | "type": "special" 710 | } 711 | ], 712 | "thresholds": { 713 | "mode": "absolute", 714 | "steps": [ 715 | { 716 | "color": "green", 717 | "value": null 718 | }, 719 | { 720 | "color": "red", 721 | "value": 80 722 | } 723 | ] 724 | }, 725 | "unit": "none" 726 | }, 727 | "overrides": [] 728 | }, 729 | "gridPos": { 730 | "h": 5, 731 | "w": 21, 732 | "x": 3, 733 | "y": 5 734 | }, 735 | "id": 34, 736 | "links": [], 737 | "maxDataPoints": 100, 738 | "options": { 739 | "colorMode": "none", 740 | "graphMode": "none", 741 | "justifyMode": "auto", 742 | "orientation": "horizontal", 743 | "reduceOptions": { 744 | "calcs": [ 745 | "lastNotNull" 746 | ], 747 | "fields": "", 748 | "values": false 749 | }, 750 | "textMode": "auto" 751 | }, 752 | "pluginVersion": "10.2.0", 753 | "targets": [ 754 | { 755 | "datasource": { 756 | "type": "prometheus", 757 | "uid": "${DS_PROMETHEUS}" 758 | }, 759 | "expr": "syncthing_rest_svc_report_node_info", 760 | "refId": "A" 761 | } 762 | ], 763 | "title": "My client version", 764 | "type": "stat" 765 | }, 766 | { 767 | "aliasColors": {}, 768 | "bars": false, 769 | "dashLength": 10, 770 | "dashes": false, 771 | "datasource": { 772 | "type": "prometheus", 773 | "uid": "${DS_PROMETHEUS}" 774 | }, 775 | "fieldConfig": { 776 | "defaults": { 777 | "links": [] 778 | }, 779 | "overrides": [] 780 | }, 781 | "fill": 3, 782 | "fillGradient": 0, 783 | "gridPos": { 784 | "h": 9, 785 | "w": 12, 786 | "x": 0, 787 | "y": 10 788 | }, 789 | "hiddenSeries": false, 790 | "id": 2, 791 | "legend": { 792 | "avg": false, 793 | "current": false, 794 | "max": false, 795 | "min": false, 796 | "show": true, 797 | "total": false, 798 | "values": false 799 | }, 800 | "lines": true, 801 | "linewidth": 1, 802 | "nullPointMode": "null", 803 | "options": { 804 | "alertThreshold": true 805 | }, 806 | "percentage": false, 807 | "pluginVersion": "10.2.0", 808 | "pointradius": 2, 809 | "points": false, 810 | "renderer": "flot", 811 | "seriesOverrides": [], 812 | "spaceLength": 10, 813 | "stack": false, 814 | "steppedLine": false, 815 | "targets": [ 816 | { 817 | "datasource": { 818 | "type": "prometheus", 819 | "uid": "${DS_PROMETHEUS}" 820 | }, 821 | "expr": "syncthing_rest_svc_report_sha256_performance", 822 | "legendFormat": "{{service}}", 823 | "refId": "A" 824 | } 825 | ], 826 | "thresholds": [], 827 | "timeRegions": [], 828 | "title": "SHA256 Performance value", 829 | "tooltip": { 830 | "shared": true, 831 | "sort": 0, 832 | "value_type": "individual" 833 | }, 834 | "type": "graph", 835 | "xaxis": { 836 | "mode": "time", 837 | "show": true, 838 | "values": [] 839 | }, 840 | "yaxes": [ 841 | { 842 | "format": "short", 843 | "logBase": 1, 844 | "show": true 845 | }, 846 | { 847 | "format": "short", 848 | "logBase": 1, 849 | "show": true 850 | } 851 | ], 852 | "yaxis": { 853 | "align": false 854 | } 855 | }, 856 | { 857 | "aliasColors": {}, 858 | "bars": false, 859 | "dashLength": 10, 860 | "dashes": false, 861 | "datasource": { 862 | "type": "prometheus", 863 | "uid": "${DS_PROMETHEUS}" 864 | }, 865 | "fieldConfig": { 866 | "defaults": { 867 | "links": [] 868 | }, 869 | "overrides": [] 870 | }, 871 | "fill": 3, 872 | "fillGradient": 0, 873 | "gridPos": { 874 | "h": 9, 875 | "w": 12, 876 | "x": 12, 877 | "y": 10 878 | }, 879 | "hiddenSeries": false, 880 | "id": 26, 881 | "legend": { 882 | "avg": false, 883 | "current": false, 884 | "max": false, 885 | "min": false, 886 | "show": true, 887 | "total": false, 888 | "values": false 889 | }, 890 | "lines": true, 891 | "linewidth": 1, 892 | "nullPointMode": "null", 893 | "options": { 894 | "alertThreshold": true 895 | }, 896 | "percentage": false, 897 | "pluginVersion": "10.2.0", 898 | "pointradius": 2, 899 | "points": false, 900 | "renderer": "flot", 901 | "seriesOverrides": [], 902 | "spaceLength": 10, 903 | "stack": false, 904 | "steppedLine": false, 905 | "targets": [ 906 | { 907 | "datasource": { 908 | "type": "prometheus", 909 | "uid": "${DS_PROMETHEUS}" 910 | }, 911 | "expr": "syncthing_rest_svc_report_memory_usage_mb", 912 | "legendFormat": "{{syncthing_data}}", 913 | "refId": "A" 914 | } 915 | ], 916 | "thresholds": [], 917 | "timeRegions": [], 918 | "title": "Memory usage by syncthc in MB", 919 | "tooltip": { 920 | "shared": true, 921 | "sort": 0, 922 | "value_type": "individual" 923 | }, 924 | "type": "graph", 925 | "xaxis": { 926 | "mode": "time", 927 | "show": true, 928 | "values": [] 929 | }, 930 | "yaxes": [ 931 | { 932 | "format": "short", 933 | "logBase": 1, 934 | "show": true 935 | }, 936 | { 937 | "format": "short", 938 | "logBase": 1, 939 | "show": true 940 | } 941 | ], 942 | "yaxis": { 943 | "align": false 944 | } 945 | }, 946 | { 947 | "datasource": { 948 | "type": "prometheus", 949 | "uid": "${DS_PROMETHEUS}" 950 | }, 951 | "fieldConfig": { 952 | "defaults": { 953 | "color": { 954 | "mode": "thresholds" 955 | }, 956 | "mappings": [ 957 | { 958 | "options": { 959 | "0": { 960 | "text": "Failed" 961 | }, 962 | "1": { 963 | "text": "OK" 964 | } 965 | }, 966 | "type": "value" 967 | } 968 | ], 969 | "thresholds": { 970 | "mode": "absolute", 971 | "steps": [ 972 | { 973 | "color": "#d44a3a", 974 | "value": null 975 | }, 976 | { 977 | "color": "rgba(237, 129, 40, 0.89)", 978 | "value": 0 979 | }, 980 | { 981 | "color": "#299c46", 982 | "value": 1 983 | } 984 | ] 985 | }, 986 | "unit": "none" 987 | }, 988 | "overrides": [] 989 | }, 990 | "gridPos": { 991 | "h": 5, 992 | "w": 5, 993 | "x": 0, 994 | "y": 19 995 | }, 996 | "id": 10, 997 | "links": [], 998 | "maxDataPoints": 100, 999 | "options": { 1000 | "colorMode": "background", 1001 | "graphMode": "none", 1002 | "justifyMode": "auto", 1003 | "orientation": "horizontal", 1004 | "reduceOptions": { 1005 | "calcs": [ 1006 | "lastNotNull" 1007 | ], 1008 | "fields": "", 1009 | "values": false 1010 | }, 1011 | "textMode": "auto" 1012 | }, 1013 | "pluginVersion": "10.2.0", 1014 | "targets": [ 1015 | { 1016 | "datasource": { 1017 | "type": "prometheus", 1018 | "uid": "${DS_PROMETHEUS}" 1019 | }, 1020 | "expr": "syncthing_rest_system_connections_remote_device_connection_info{deviceID=\"$deviceID\"}", 1021 | "refId": "A" 1022 | } 1023 | ], 1024 | "title": "Information of the connection with remote device", 1025 | "type": "stat" 1026 | }, 1027 | { 1028 | "datasource": { 1029 | "type": "prometheus", 1030 | "uid": "${DS_PROMETHEUS}" 1031 | }, 1032 | "fieldConfig": { 1033 | "defaults": { 1034 | "color": { 1035 | "mode": "thresholds" 1036 | }, 1037 | "mappings": [ 1038 | { 1039 | "options": { 1040 | "0": { 1041 | "text": "Not paused" 1042 | }, 1043 | "1": { 1044 | "text": "Paused" 1045 | } 1046 | }, 1047 | "type": "value" 1048 | }, 1049 | { 1050 | "options": { 1051 | "match": "null", 1052 | "result": { 1053 | "text": "No data" 1054 | } 1055 | }, 1056 | "type": "special" 1057 | } 1058 | ], 1059 | "thresholds": { 1060 | "mode": "absolute", 1061 | "steps": [ 1062 | { 1063 | "color": "green", 1064 | "value": null 1065 | }, 1066 | { 1067 | "color": "red", 1068 | "value": 80 1069 | } 1070 | ] 1071 | }, 1072 | "unit": "none" 1073 | }, 1074 | "overrides": [] 1075 | }, 1076 | "gridPos": { 1077 | "h": 5, 1078 | "w": 2, 1079 | "x": 5, 1080 | "y": 19 1081 | }, 1082 | "id": 20, 1083 | "links": [], 1084 | "maxDataPoints": 100, 1085 | "options": { 1086 | "colorMode": "background", 1087 | "graphMode": "none", 1088 | "justifyMode": "auto", 1089 | "orientation": "horizontal", 1090 | "reduceOptions": { 1091 | "calcs": [ 1092 | "lastNotNull" 1093 | ], 1094 | "fields": "", 1095 | "values": false 1096 | }, 1097 | "textMode": "auto" 1098 | }, 1099 | "pluginVersion": "10.2.0", 1100 | "targets": [ 1101 | { 1102 | "datasource": { 1103 | "type": "prometheus", 1104 | "uid": "${DS_PROMETHEUS}" 1105 | }, 1106 | "expr": "syncthing_rest_system_connections_remote_device_is_paused{deviceID=\"$deviceID\"}", 1107 | "refId": "A" 1108 | } 1109 | ], 1110 | "title": "Is sync paused", 1111 | "type": "stat" 1112 | }, 1113 | { 1114 | "datasource": { 1115 | "type": "prometheus", 1116 | "uid": "${DS_PROMETHEUS}" 1117 | }, 1118 | "fieldConfig": { 1119 | "defaults": { 1120 | "color": { 1121 | "mode": "thresholds" 1122 | }, 1123 | "mappings": [ 1124 | { 1125 | "options": { 1126 | "0": { 1127 | "text": "No data" 1128 | }, 1129 | "1": { 1130 | "text": "$clientVersion" 1131 | } 1132 | }, 1133 | "type": "value" 1134 | } 1135 | ], 1136 | "thresholds": { 1137 | "mode": "absolute", 1138 | "steps": [ 1139 | { 1140 | "color": "green", 1141 | "value": null 1142 | }, 1143 | { 1144 | "color": "red", 1145 | "value": 80 1146 | } 1147 | ] 1148 | }, 1149 | "unit": "none" 1150 | }, 1151 | "overrides": [] 1152 | }, 1153 | "gridPos": { 1154 | "h": 5, 1155 | "w": 3, 1156 | "x": 7, 1157 | "y": 19 1158 | }, 1159 | "id": 12, 1160 | "links": [], 1161 | "maxDataPoints": 100, 1162 | "options": { 1163 | "colorMode": "none", 1164 | "graphMode": "none", 1165 | "justifyMode": "auto", 1166 | "orientation": "horizontal", 1167 | "reduceOptions": { 1168 | "calcs": [ 1169 | "lastNotNull" 1170 | ], 1171 | "fields": "", 1172 | "values": false 1173 | }, 1174 | "textMode": "auto" 1175 | }, 1176 | "pluginVersion": "10.2.0", 1177 | "targets": [ 1178 | { 1179 | "datasource": { 1180 | "type": "prometheus", 1181 | "uid": "${DS_PROMETHEUS}" 1182 | }, 1183 | "editorMode": "code", 1184 | "exemplar": true, 1185 | "expr": "syncthing_rest_system_connections_remote_device_connection_info{deviceID=\"$deviceID\"}", 1186 | "interval": "", 1187 | "legendFormat": "", 1188 | "range": true, 1189 | "refId": "A" 1190 | } 1191 | ], 1192 | "title": "Syncthing client version", 1193 | "type": "stat" 1194 | }, 1195 | { 1196 | "datasource": { 1197 | "type": "prometheus", 1198 | "uid": "${DS_PROMETHEUS}" 1199 | }, 1200 | "fieldConfig": { 1201 | "defaults": { 1202 | "color": { 1203 | "mode": "thresholds" 1204 | }, 1205 | "mappings": [ 1206 | { 1207 | "options": { 1208 | "0": { 1209 | "text": "No data" 1210 | }, 1211 | "1": { 1212 | "text": "$crypto" 1213 | } 1214 | }, 1215 | "type": "value" 1216 | } 1217 | ], 1218 | "thresholds": { 1219 | "mode": "absolute", 1220 | "steps": [ 1221 | { 1222 | "color": "green", 1223 | "value": null 1224 | }, 1225 | { 1226 | "color": "red", 1227 | "value": 80 1228 | } 1229 | ] 1230 | }, 1231 | "unit": "none" 1232 | }, 1233 | "overrides": [] 1234 | }, 1235 | "gridPos": { 1236 | "h": 5, 1237 | "w": 5, 1238 | "x": 10, 1239 | "y": 19 1240 | }, 1241 | "id": 14, 1242 | "links": [], 1243 | "maxDataPoints": 100, 1244 | "options": { 1245 | "colorMode": "none", 1246 | "graphMode": "none", 1247 | "justifyMode": "auto", 1248 | "orientation": "horizontal", 1249 | "reduceOptions": { 1250 | "calcs": [ 1251 | "lastNotNull" 1252 | ], 1253 | "fields": "", 1254 | "values": false 1255 | }, 1256 | "textMode": "auto" 1257 | }, 1258 | "pluginVersion": "10.2.0", 1259 | "targets": [ 1260 | { 1261 | "datasource": { 1262 | "type": "prometheus", 1263 | "uid": "${DS_PROMETHEUS}" 1264 | }, 1265 | "exemplar": true, 1266 | "expr": "syncthing_rest_system_connections_remote_device_connection_info{deviceID=\"$deviceID\"}", 1267 | "interval": "", 1268 | "legendFormat": "", 1269 | "refId": "A" 1270 | } 1271 | ], 1272 | "title": "Crypto configuration", 1273 | "type": "stat" 1274 | }, 1275 | { 1276 | "datasource": { 1277 | "type": "prometheus", 1278 | "uid": "${DS_PROMETHEUS}" 1279 | }, 1280 | "fieldConfig": { 1281 | "defaults": { 1282 | "color": { 1283 | "mode": "thresholds" 1284 | }, 1285 | "mappings": [ 1286 | { 1287 | "options": { 1288 | "match": "null", 1289 | "result": { 1290 | "text": "No data" 1291 | } 1292 | }, 1293 | "type": "special" 1294 | } 1295 | ], 1296 | "thresholds": { 1297 | "mode": "absolute", 1298 | "steps": [ 1299 | { 1300 | "color": "green", 1301 | "value": null 1302 | }, 1303 | { 1304 | "color": "red", 1305 | "value": 80 1306 | } 1307 | ] 1308 | }, 1309 | "unit": "bytes" 1310 | }, 1311 | "overrides": [] 1312 | }, 1313 | "gridPos": { 1314 | "h": 5, 1315 | "w": 4, 1316 | "x": 15, 1317 | "y": 19 1318 | }, 1319 | "id": 18, 1320 | "links": [], 1321 | "maxDataPoints": 100, 1322 | "options": { 1323 | "colorMode": "none", 1324 | "graphMode": "none", 1325 | "justifyMode": "auto", 1326 | "orientation": "horizontal", 1327 | "reduceOptions": { 1328 | "calcs": [ 1329 | "lastNotNull" 1330 | ], 1331 | "fields": "", 1332 | "values": false 1333 | }, 1334 | "textMode": "auto" 1335 | }, 1336 | "pluginVersion": "10.2.0", 1337 | "targets": [ 1338 | { 1339 | "datasource": { 1340 | "type": "prometheus", 1341 | "uid": "${DS_PROMETHEUS}" 1342 | }, 1343 | "expr": "syncthing_rest_system_connections_remote_device_out_bytes_total{deviceID=\"$deviceID\"}", 1344 | "refId": "A" 1345 | } 1346 | ], 1347 | "title": "Total bytes transmitted to remote device", 1348 | "type": "stat" 1349 | }, 1350 | { 1351 | "datasource": { 1352 | "type": "prometheus", 1353 | "uid": "${DS_PROMETHEUS}" 1354 | }, 1355 | "fieldConfig": { 1356 | "defaults": { 1357 | "color": { 1358 | "mode": "thresholds" 1359 | }, 1360 | "mappings": [ 1361 | { 1362 | "options": { 1363 | "match": "null", 1364 | "result": { 1365 | "text": "No data" 1366 | } 1367 | }, 1368 | "type": "special" 1369 | } 1370 | ], 1371 | "thresholds": { 1372 | "mode": "absolute", 1373 | "steps": [ 1374 | { 1375 | "color": "green", 1376 | "value": null 1377 | }, 1378 | { 1379 | "color": "red", 1380 | "value": 80 1381 | } 1382 | ] 1383 | }, 1384 | "unit": "bytes" 1385 | }, 1386 | "overrides": [] 1387 | }, 1388 | "gridPos": { 1389 | "h": 5, 1390 | "w": 5, 1391 | "x": 19, 1392 | "y": 19 1393 | }, 1394 | "id": 16, 1395 | "links": [], 1396 | "maxDataPoints": 100, 1397 | "options": { 1398 | "colorMode": "none", 1399 | "graphMode": "none", 1400 | "justifyMode": "auto", 1401 | "orientation": "horizontal", 1402 | "reduceOptions": { 1403 | "calcs": [ 1404 | "lastNotNull" 1405 | ], 1406 | "fields": "", 1407 | "values": false 1408 | }, 1409 | "textMode": "auto" 1410 | }, 1411 | "pluginVersion": "10.2.0", 1412 | "targets": [ 1413 | { 1414 | "datasource": { 1415 | "type": "prometheus", 1416 | "uid": "${DS_PROMETHEUS}" 1417 | }, 1418 | "expr": "syncthing_rest_system_connections_remote_device_in_bytes_total{deviceID=\"$deviceID\"}", 1419 | "refId": "A" 1420 | } 1421 | ], 1422 | "title": "Total bytes received from remote device", 1423 | "type": "stat" 1424 | }, 1425 | { 1426 | "aliasColors": {}, 1427 | "bars": false, 1428 | "dashLength": 10, 1429 | "dashes": false, 1430 | "datasource": { 1431 | "type": "prometheus", 1432 | "uid": "${DS_PROMETHEUS}" 1433 | }, 1434 | "fill": 1, 1435 | "fillGradient": 0, 1436 | "gridPos": { 1437 | "h": 5, 1438 | "w": 10, 1439 | "x": 0, 1440 | "y": 24 1441 | }, 1442 | "hiddenSeries": false, 1443 | "id": 36, 1444 | "legend": { 1445 | "avg": false, 1446 | "current": false, 1447 | "max": false, 1448 | "min": false, 1449 | "show": true, 1450 | "total": false, 1451 | "values": false 1452 | }, 1453 | "lines": true, 1454 | "linewidth": 1, 1455 | "nullPointMode": "null", 1456 | "options": { 1457 | "alertThreshold": true 1458 | }, 1459 | "percentage": false, 1460 | "pluginVersion": "10.2.0", 1461 | "pointradius": 2, 1462 | "points": false, 1463 | "renderer": "flot", 1464 | "seriesOverrides": [], 1465 | "spaceLength": 10, 1466 | "stack": false, 1467 | "steppedLine": false, 1468 | "targets": [ 1469 | { 1470 | "datasource": { 1471 | "type": "prometheus", 1472 | "uid": "${DS_PROMETHEUS}" 1473 | }, 1474 | "exemplar": true, 1475 | "expr": "syncthing_rest_db_status_global_total_items{folderID=\"$folderID\"}", 1476 | "interval": "", 1477 | "legendFormat": "Global total items", 1478 | "refId": "A" 1479 | }, 1480 | { 1481 | "datasource": { 1482 | "type": "prometheus", 1483 | "uid": "${DS_PROMETHEUS}" 1484 | }, 1485 | "exemplar": true, 1486 | "expr": "syncthing_rest_db_status_local_total_items{folderID=\"$folderID\"}", 1487 | "hide": false, 1488 | "interval": "", 1489 | "legendFormat": "Local total items", 1490 | "refId": "B" 1491 | }, 1492 | { 1493 | "datasource": { 1494 | "type": "prometheus", 1495 | "uid": "${DS_PROMETHEUS}" 1496 | }, 1497 | "exemplar": true, 1498 | "expr": "syncthing_rest_db_status_need_total_items{folderID=\"$folderID\"}", 1499 | "hide": false, 1500 | "interval": "", 1501 | "legendFormat": "Need total items", 1502 | "refId": "C" 1503 | }, 1504 | { 1505 | "datasource": { 1506 | "type": "prometheus", 1507 | "uid": "${DS_PROMETHEUS}" 1508 | }, 1509 | "exemplar": true, 1510 | "expr": "syncthing_rest_db_status_insync_files{folderID=\"$folderID\"}", 1511 | "hide": false, 1512 | "interval": "", 1513 | "legendFormat": "Insync Files", 1514 | "refId": "D" 1515 | } 1516 | ], 1517 | "thresholds": [], 1518 | "timeRegions": [], 1519 | "title": "Items by folderID", 1520 | "tooltip": { 1521 | "shared": true, 1522 | "sort": 0, 1523 | "value_type": "individual" 1524 | }, 1525 | "type": "graph", 1526 | "xaxis": { 1527 | "mode": "time", 1528 | "show": true, 1529 | "values": [] 1530 | }, 1531 | "yaxes": [ 1532 | { 1533 | "format": "short", 1534 | "logBase": 1, 1535 | "show": true 1536 | }, 1537 | { 1538 | "format": "short", 1539 | "logBase": 1, 1540 | "show": true 1541 | } 1542 | ], 1543 | "yaxis": { 1544 | "align": false 1545 | } 1546 | }, 1547 | { 1548 | "datasource": { 1549 | "type": "prometheus", 1550 | "uid": "${DS_PROMETHEUS}" 1551 | }, 1552 | "fieldConfig": { 1553 | "defaults": { 1554 | "color": { 1555 | "mode": "thresholds" 1556 | }, 1557 | "mappings": [ 1558 | { 1559 | "options": { 1560 | "from": 0, 1561 | "result": { 1562 | "text": "$state" 1563 | }, 1564 | "to": 99999999999999 1565 | }, 1566 | "type": "range" 1567 | } 1568 | ], 1569 | "thresholds": { 1570 | "mode": "absolute", 1571 | "steps": [ 1572 | { 1573 | "color": "green", 1574 | "value": null 1575 | }, 1576 | { 1577 | "color": "red", 1578 | "value": 80 1579 | } 1580 | ] 1581 | }, 1582 | "unit": "none" 1583 | }, 1584 | "overrides": [] 1585 | }, 1586 | "gridPos": { 1587 | "h": 5, 1588 | "w": 3, 1589 | "x": 10, 1590 | "y": 24 1591 | }, 1592 | "id": 38, 1593 | "links": [], 1594 | "maxDataPoints": 100, 1595 | "options": { 1596 | "colorMode": "none", 1597 | "graphMode": "none", 1598 | "justifyMode": "auto", 1599 | "orientation": "horizontal", 1600 | "reduceOptions": { 1601 | "calcs": [ 1602 | "lastNotNull" 1603 | ], 1604 | "fields": "", 1605 | "values": false 1606 | }, 1607 | "textMode": "auto" 1608 | }, 1609 | "pluginVersion": "10.2.0", 1610 | "targets": [ 1611 | { 1612 | "datasource": { 1613 | "type": "prometheus", 1614 | "uid": "${DS_PROMETHEUS}" 1615 | }, 1616 | "exemplar": true, 1617 | "expr": "syncthing_rest_db_status_sequence{folderID=\"$folderID\"}", 1618 | "interval": "", 1619 | "legendFormat": "", 1620 | "refId": "A" 1621 | } 1622 | ], 1623 | "title": "Folder state", 1624 | "type": "stat" 1625 | }, 1626 | { 1627 | "datasource": { 1628 | "type": "prometheus", 1629 | "uid": "${DS_PROMETHEUS}" 1630 | }, 1631 | "fieldConfig": { 1632 | "defaults": { 1633 | "color": { 1634 | "mode": "thresholds" 1635 | }, 1636 | "mappings": [ 1637 | { 1638 | "options": { 1639 | "from": 0, 1640 | "result": { 1641 | "text": "$stateChanged" 1642 | }, 1643 | "to": 99999999999999 1644 | }, 1645 | "type": "range" 1646 | } 1647 | ], 1648 | "thresholds": { 1649 | "mode": "absolute", 1650 | "steps": [ 1651 | { 1652 | "color": "green", 1653 | "value": null 1654 | }, 1655 | { 1656 | "color": "red", 1657 | "value": 80 1658 | } 1659 | ] 1660 | }, 1661 | "unit": "none" 1662 | }, 1663 | "overrides": [] 1664 | }, 1665 | "gridPos": { 1666 | "h": 5, 1667 | "w": 11, 1668 | "x": 13, 1669 | "y": 24 1670 | }, 1671 | "id": 40, 1672 | "links": [], 1673 | "maxDataPoints": 100, 1674 | "options": { 1675 | "colorMode": "none", 1676 | "graphMode": "none", 1677 | "justifyMode": "auto", 1678 | "orientation": "horizontal", 1679 | "reduceOptions": { 1680 | "calcs": [ 1681 | "lastNotNull" 1682 | ], 1683 | "fields": "", 1684 | "values": false 1685 | }, 1686 | "textMode": "auto" 1687 | }, 1688 | "pluginVersion": "10.2.0", 1689 | "targets": [ 1690 | { 1691 | "datasource": { 1692 | "type": "prometheus", 1693 | "uid": "${DS_PROMETHEUS}" 1694 | }, 1695 | "exemplar": true, 1696 | "expr": "syncthing_rest_db_status_sequence{folderID=\"$folderID\"}", 1697 | "interval": "", 1698 | "legendFormat": "", 1699 | "refId": "A" 1700 | } 1701 | ], 1702 | "title": "Folder state changed", 1703 | "type": "stat" 1704 | } 1705 | ], 1706 | "refresh": "", 1707 | "schemaVersion": 38, 1708 | "tags": [], 1709 | "templating": { 1710 | "list": [ 1711 | { 1712 | "current": {}, 1713 | "datasource": { 1714 | "type": "prometheus", 1715 | "uid": "${DS_PROMETHEUS}" 1716 | }, 1717 | "definition": "syncthing_rest_system_connections_remote_device_connection_info", 1718 | "hide": 0, 1719 | "includeAll": false, 1720 | "label": "deviceID", 1721 | "multi": false, 1722 | "name": "deviceID", 1723 | "options": [], 1724 | "query": { 1725 | "query": "syncthing_rest_system_connections_remote_device_connection_info", 1726 | "refId": "Prometheus-Local-deviceID-Variable-Query" 1727 | }, 1728 | "refresh": 1, 1729 | "regex": "/.*deviceID=\"([^\"]*).*/", 1730 | "skipUrlSync": false, 1731 | "sort": 0, 1732 | "tagValuesQuery": "", 1733 | "tagsQuery": "", 1734 | "type": "query", 1735 | "useTags": false 1736 | }, 1737 | { 1738 | "current": {}, 1739 | "datasource": { 1740 | "type": "prometheus", 1741 | "uid": "${DS_PROMETHEUS}" 1742 | }, 1743 | "definition": "syncthing_rest_system_connections_remote_device_connection_info", 1744 | "hide": 2, 1745 | "includeAll": false, 1746 | "multi": false, 1747 | "name": "clientVersion", 1748 | "options": [], 1749 | "query": { 1750 | "query": "syncthing_rest_system_connections_remote_device_connection_info", 1751 | "refId": "Prometheus-Local-clientVersion-Variable-Query" 1752 | }, 1753 | "refresh": 2, 1754 | "regex": "/.*clientVersion=\"([^\"]*).*/", 1755 | "skipUrlSync": false, 1756 | "sort": 0, 1757 | "tagValuesQuery": "", 1758 | "tagsQuery": "", 1759 | "type": "query", 1760 | "useTags": false 1761 | }, 1762 | { 1763 | "current": {}, 1764 | "datasource": { 1765 | "type": "prometheus", 1766 | "uid": "${DS_PROMETHEUS}" 1767 | }, 1768 | "definition": "syncthing_rest_system_connections_remote_device_connection_info", 1769 | "hide": 2, 1770 | "includeAll": false, 1771 | "label": "crypto", 1772 | "multi": false, 1773 | "name": "crypto", 1774 | "options": [], 1775 | "query": { 1776 | "query": "syncthing_rest_system_connections_remote_device_connection_info", 1777 | "refId": "Prometheus-Local-crypto-Variable-Query" 1778 | }, 1779 | "refresh": 2, 1780 | "regex": "/.*crypto=\"([^\"]*).*/", 1781 | "skipUrlSync": false, 1782 | "sort": 0, 1783 | "tagValuesQuery": "", 1784 | "tagsQuery": "", 1785 | "type": "query", 1786 | "useTags": false 1787 | }, 1788 | { 1789 | "current": {}, 1790 | "datasource": { 1791 | "type": "prometheus", 1792 | "uid": "${DS_PROMETHEUS}" 1793 | }, 1794 | "definition": "", 1795 | "hide": 2, 1796 | "includeAll": false, 1797 | "label": "myClientLongVersion", 1798 | "multi": false, 1799 | "name": "myClientLongVersion", 1800 | "options": [], 1801 | "query": { 1802 | "qryType": 5, 1803 | "query": "", 1804 | "refId": "PrometheusVariableQueryEditor-VariableQuery" 1805 | }, 1806 | "refresh": 2, 1807 | "regex": "/.*longVersion=\"([^,]*)\".*/", 1808 | "skipUrlSync": false, 1809 | "sort": 0, 1810 | "tagValuesQuery": "", 1811 | "tagsQuery": "", 1812 | "type": "query", 1813 | "useTags": false 1814 | }, 1815 | { 1816 | "current": {}, 1817 | "datasource": { 1818 | "type": "prometheus", 1819 | "uid": "${DS_PROMETHEUS}" 1820 | }, 1821 | "definition": "syncthing_rest_db_status_sequence", 1822 | "hide": 0, 1823 | "includeAll": false, 1824 | "label": "folderID", 1825 | "multi": false, 1826 | "name": "folderID", 1827 | "options": [], 1828 | "query": { 1829 | "query": "syncthing_rest_db_status_sequence", 1830 | "refId": "StandardVariableQuery" 1831 | }, 1832 | "refresh": 1, 1833 | "regex": "/.*folderID=\"([^\"]*).*/", 1834 | "skipUrlSync": false, 1835 | "sort": 0, 1836 | "tagValuesQuery": "", 1837 | "tagsQuery": "", 1838 | "type": "query", 1839 | "useTags": false 1840 | }, 1841 | { 1842 | "current": {}, 1843 | "datasource": { 1844 | "type": "prometheus", 1845 | "uid": "${DS_PROMETHEUS}" 1846 | }, 1847 | "definition": "syncthing_rest_db_status_sequence", 1848 | "hide": 2, 1849 | "includeAll": false, 1850 | "multi": false, 1851 | "name": "state", 1852 | "options": [], 1853 | "query": { 1854 | "query": "syncthing_rest_db_status_sequence", 1855 | "refId": "StandardVariableQuery" 1856 | }, 1857 | "refresh": 2, 1858 | "regex": "/.*state=\"([^\"]*).*/", 1859 | "skipUrlSync": false, 1860 | "sort": 0, 1861 | "tagValuesQuery": "", 1862 | "tagsQuery": "", 1863 | "type": "query", 1864 | "useTags": false 1865 | }, 1866 | { 1867 | "current": {}, 1868 | "datasource": { 1869 | "type": "prometheus", 1870 | "uid": "${DS_PROMETHEUS}" 1871 | }, 1872 | "definition": "syncthing_rest_db_status_sequence", 1873 | "hide": 2, 1874 | "includeAll": false, 1875 | "multi": false, 1876 | "name": "stateChanged", 1877 | "options": [], 1878 | "query": { 1879 | "query": "syncthing_rest_db_status_sequence", 1880 | "refId": "StandardVariableQuery" 1881 | }, 1882 | "refresh": 2, 1883 | "regex": "/.*stateChanged=\"([^\"]*).*/", 1884 | "skipUrlSync": false, 1885 | "sort": 0, 1886 | "tagValuesQuery": "", 1887 | "tagsQuery": "", 1888 | "type": "query", 1889 | "useTags": false 1890 | } 1891 | ] 1892 | }, 1893 | "time": { 1894 | "from": "now-24h", 1895 | "to": "now" 1896 | }, 1897 | "timepicker": { 1898 | "refresh_intervals": [ 1899 | "5s", 1900 | "10s", 1901 | "30s", 1902 | "1m", 1903 | "5m", 1904 | "15m", 1905 | "30m", 1906 | "1h", 1907 | "2h", 1908 | "1d" 1909 | ] 1910 | }, 1911 | "timezone": "", 1912 | "title": "Syncthing Exporter 0.3.6", 1913 | "uid": "UurBkpuGK", 1914 | "version": 1, 1915 | "weekStart": "" 1916 | } -------------------------------------------------------------------------------- /examples/grafana/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f100024/syncthing_exporter/72b258f1a7a9e7d62beb0ee2a49f16874981f8fd/examples/grafana/screenshot-1.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/f100024/syncthing_exporter 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/alecthomas/kingpin/v2 v2.4.0 7 | github.com/prometheus/client_golang v1.22.0 8 | github.com/prometheus/common v0.63.0 9 | github.com/syncthing/syncthing v1.29.4 10 | ) 11 | 12 | require ( 13 | github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect 14 | github.com/beorn7/perks v1.0.1 // indirect 15 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 16 | github.com/ebitengine/purego v0.8.2 // indirect 17 | github.com/go-ole/go-ole v1.3.0 // indirect 18 | github.com/golang/snappy v1.0.0 // indirect 19 | github.com/greatroar/blobloom v0.8.0 // indirect 20 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 21 | github.com/kylelemons/godebug v1.1.0 // indirect 22 | github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect 23 | github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75 // indirect 24 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 25 | github.com/pierrec/lz4/v4 v4.1.22 // indirect 26 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 27 | github.com/prometheus/client_model v0.6.2 // indirect 28 | github.com/prometheus/procfs v0.16.0 // indirect 29 | github.com/shirou/gopsutil/v4 v4.25.3 // indirect 30 | github.com/syncthing/notify v0.0.0-20250207082249-f0fa8f99c2bc // indirect 31 | github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect 32 | github.com/thejerf/suture/v4 v4.0.6 // indirect 33 | github.com/tklauser/go-sysconf v0.3.15 // indirect 34 | github.com/tklauser/numcpus v0.10.0 // indirect 35 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 36 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 37 | golang.org/x/crypto v0.37.0 // indirect 38 | golang.org/x/net v0.39.0 // indirect 39 | golang.org/x/sys v0.32.0 // indirect 40 | golang.org/x/text v0.24.0 // indirect 41 | google.golang.org/protobuf v1.36.6 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= 2 | github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= 3 | github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= 4 | github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= 5 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 6 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 7 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 8 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 10 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 11 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 12 | github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U= 13 | github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= 18 | github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 19 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 20 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 21 | github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= 22 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 23 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 24 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 25 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 26 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 27 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 28 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 29 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 30 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 31 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 32 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 33 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 34 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 35 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 36 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 37 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 38 | github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= 39 | github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 40 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 41 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 42 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 43 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 44 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 45 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 46 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 47 | github.com/greatroar/blobloom v0.8.0 h1:I9RlEkfqK9/6f1v9mFmDYegDQ/x0mISCpiNpAm23Pt4= 48 | github.com/greatroar/blobloom v0.8.0/go.mod h1:mjMJ1hh1wjGVfr93QIHJ6FfDNVrA0IELv8OvMHJxHKs= 49 | github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 50 | github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 51 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 52 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 53 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 54 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 55 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 56 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 57 | github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= 58 | github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= 59 | github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75 h1:cUVxyR+UfmdEAZGJ8IiKld1O0dbGotEnkMolG5hfMSY= 60 | github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75/go.mod h1:pBbZyGwC5i16IBkjVKoy/sznA8jPD/K9iedwe1ESE6w= 61 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 62 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 63 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 64 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 65 | github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= 66 | github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= 67 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 68 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 69 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 70 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 71 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 72 | github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 73 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 74 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 75 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 76 | github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= 77 | github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= 78 | github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= 79 | github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 80 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 81 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 82 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= 83 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 84 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 85 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 86 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 87 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 88 | github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= 89 | github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 90 | github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= 91 | github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= 92 | github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE= 93 | github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= 94 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 95 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 96 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 97 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 98 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 99 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 100 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 101 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 102 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 103 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 104 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 105 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 106 | github.com/syncthing/notify v0.0.0-20250207082249-f0fa8f99c2bc h1:xc3UfSFlH/X5hRw3h21RF6WXnRUYKmGRx06FEaVxfkM= 107 | github.com/syncthing/notify v0.0.0-20250207082249-f0fa8f99c2bc/go.mod h1:J0q59IWjLtpRIJulohwqEZvjzwOfTEPp8SVhDJl+y0Y= 108 | github.com/syncthing/syncthing v1.29.4 h1:eUdr5tEGyh+eH8XSRG75scHFhtVj4c6XoaLjaiMqpmk= 109 | github.com/syncthing/syncthing v1.29.4/go.mod h1:4uWzdBhnOb1g4lR0XNfAq0aDdYns4wr9BeR1RDbnzEk= 110 | github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= 111 | github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= 112 | github.com/thejerf/suture/v4 v4.0.6 h1:QsuCEsCqb03xF9tPAsWAj8QOAJBgQI1c0VqJNaingg8= 113 | github.com/thejerf/suture/v4 v4.0.6/go.mod h1:gu9Y4dXNUWFrByqRt30Rm9/UZ0wzRSt9AJS6xu/ZGxU= 114 | github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= 115 | github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= 116 | github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= 117 | github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= 118 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= 119 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= 120 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 121 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 122 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 123 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 124 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 125 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 126 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 127 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 128 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 129 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 130 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 131 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 132 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 133 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 134 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 135 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 136 | golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 137 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 138 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 139 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 141 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 142 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 143 | golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 144 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 145 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 148 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 150 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 151 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 152 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 153 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 154 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 155 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 156 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 157 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 158 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 159 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 160 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 161 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 162 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 163 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 164 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 165 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 166 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 167 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 168 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 169 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 170 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 171 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 172 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 173 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 174 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 175 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 176 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 177 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 178 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 179 | golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 180 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 181 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 182 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 183 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 184 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 185 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 186 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 187 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 188 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 189 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 190 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 191 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 192 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 193 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 194 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 195 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 196 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 197 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 198 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 199 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 200 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 201 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 202 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Package main is a build entry point of syncthing_exporter 2 | package main 3 | 4 | import ( 5 | "net/http" 6 | "os" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/f100024/syncthing_exporter/collector" 11 | 12 | kingpin "github.com/alecthomas/kingpin/v2" 13 | "github.com/prometheus/client_golang/prometheus" 14 | clientVersion "github.com/prometheus/client_golang/prometheus/collectors/version" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | "github.com/prometheus/common/promslog" 17 | "github.com/prometheus/common/version" 18 | ) 19 | 20 | func main() { 21 | var ( 22 | Name = "syncthing_exporter" 23 | webListenAddress = kingpin.Flag("web.listen-address", 24 | "Address ot listen on for web interface and telemetry. Environment variable: WEB_LISTEN_ADDRESS"). 25 | Default(":9093"). 26 | Envar("WEB_LISTEN_ADDRESS"). 27 | String() 28 | 29 | webMetricsPath = kingpin.Flag("web.metrics-path", 30 | "Path under which to expose metrics. Environment variable: WEB_METRIC_PATH"). 31 | Default("/metrics"). 32 | Envar("WEB_METRIC_PATH"). 33 | String() 34 | 35 | syncthingURI = kingpin.Flag("syncthing.uri", 36 | "HTTP API address of Syncthing node (e.g. http://127.0.0.1:8384). Environment variable: SYNCTHING_URI"). 37 | Required(). 38 | Envar("SYNCTHING_URI"). 39 | URL() 40 | 41 | syncthingToken = kingpin.Flag("syncthing.token", 42 | "Token for authentification Syncthing API. Environment variable: SYNCTHING_TOKEN"). 43 | Required(). 44 | Envar("SYNCTHING_TOKEN"). 45 | String() 46 | 47 | syncthingTimeout = kingpin.Flag("syncthing.timeout", 48 | "Timeout for trying to get stats from Syncthing. Environment variable: SYNCTHING_TIMEOUT"). 49 | Default("5s"). 50 | Envar("SYNCTHING_TIMEOUT"). 51 | Duration() 52 | 53 | syncthingFoldersID = kingpin.Flag("syncthing.foldersid", 54 | "ID of folders for getting db status. Environment variable: SYNCTHING_FOLDERSID"). 55 | Envar("SYNCTHING_FOLDERSID"). 56 | String() 57 | ) 58 | 59 | promslogConfig := &promslog.Config{Style: "slog"} 60 | 61 | kingpin.Version(version.Print(Name)) 62 | kingpin.CommandLine.HelpFlag.Short('h') 63 | kingpin.Parse() 64 | 65 | logger := promslog.New(promslogConfig) 66 | stURL := *syncthingURI 67 | 68 | isValidURLSchema, _ := regexp.MatchString("^(http|https)$", stURL.Scheme) 69 | if !isValidURLSchema { 70 | logger.Error("Syncthing URL schema is not allowed. URL schema should be matched http|https.") 71 | os.Exit(1) 72 | } 73 | 74 | collector.HTTPClient.Timeout = *syncthingTimeout 75 | 76 | versionMetric := clientVersion.NewCollector(Name) 77 | prometheus.MustRegister(versionMetric) 78 | 79 | prometheus.MustRegister(collector.NewSVCReport(logger, collector.HTTPClient, stURL, syncthingToken)) 80 | prometheus.MustRegister(collector.NewSCReport(logger, collector.HTTPClient, stURL, syncthingToken)) 81 | prometheus.MustRegister(collector.NewStatsDeviceReport(logger, collector.HTTPClient, stURL, syncthingToken)) 82 | prometheus.MustRegister(collector.NewConfigDevicesReport(logger, collector.HTTPClient, stURL, syncthingToken)) 83 | if *syncthingFoldersID != "" { 84 | foldersIDList := func(s *string) *[]string { 85 | var list []string 86 | splittedList := strings.Split(*s, ",") 87 | for folderid := range splittedList { 88 | list = append(list, strings.TrimSpace(splittedList[folderid])) 89 | } 90 | return &list 91 | } 92 | prometheus.MustRegister(collector.NewDBStatusReport(logger, collector.HTTPClient, stURL, syncthingToken, foldersIDList(syncthingFoldersID))) 93 | } 94 | 95 | logger.Info("Starting syncthing_exporter", "version", version.Info()) 96 | logger.Info("Build context", "build_context", version.BuildContext()) 97 | 98 | http.Handle(*webMetricsPath, promhttp.Handler()) 99 | http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { 100 | w.Write([]byte(` 101 |