├── .github ├── dependabot.yml └── workflows │ ├── docker_latest.yml │ ├── docker_tag.yml │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yml ├── CODEOWNERS ├── Dockerfile ├── LICENSE ├── LICENSES └── MIT.txt ├── README.md ├── collector.go ├── config ├── config.go ├── config_test.go ├── duration.go ├── target.go └── testdata │ └── config_test.yml ├── custom_labels.go ├── dist ├── charts │ ├── index.yaml │ ├── ping-exporter-1.0.0.tgz │ ├── ping-exporter-1.1.0.tgz │ └── ping-exporter │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── files │ │ └── prometheus.rules │ │ ├── templates │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ ├── ingress.yaml │ │ ├── prometheusrule.yaml │ │ ├── service.yaml │ │ ├── serviceaccount.yaml │ │ ├── servicemonitor.yaml │ │ └── tests │ │ │ └── test-connection.yaml │ │ └── values.yaml ├── ping_exporter.d │ ├── systemd-233.conf │ ├── systemd-235.conf │ ├── systemd-242.conf │ └── systemd-245.conf ├── ping_exporter.service ├── ping_exporter.yaml ├── postinstall.sh ├── postremove.sh └── preremove.sh ├── go.mod ├── go.sum ├── helper.go ├── main.go ├── rttscale.go ├── snap └── snapcraft.yaml ├── tailscale.go ├── target.go └── target_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/docker_latest.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_run: 3 | workflows: ["Test"] 4 | branches: [main] 5 | types: 6 | - completed 7 | 8 | name: Docker Build (latest) 9 | jobs: 10 | build: 11 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Check Out Repo 16 | uses: actions/checkout@v4 17 | 18 | - name: Login to Docker Hub 19 | uses: docker/login-action@v1 20 | with: 21 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 22 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 23 | 24 | - name: Set up Docker Buildx 25 | id: buildx 26 | uses: docker/setup-buildx-action@v1 27 | with: 28 | install: true 29 | 30 | - name: Extract repo name 31 | id: extract_repo_name 32 | shell: bash 33 | run: echo "##[set-output name=repo;]$(echo ${GITHUB_REPOSITORY#*/})" 34 | 35 | - name: Build and push 36 | id: docker_build 37 | uses: docker/build-push-action@v2 38 | with: 39 | context: ./ 40 | file: ./Dockerfile 41 | push: true 42 | tags: ${{ secrets.DOCKER_HUB_USERNAME }}/${{ steps.extract_repo_name.outputs.repo }}:latest 43 | platforms: linux/amd64,linux/arm64 44 | 45 | - name: Image digest 46 | run: echo ${{ steps.docker_build.outputs.digest }} 47 | -------------------------------------------------------------------------------- /.github/workflows/docker_tag.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - '*.*.*' 5 | 6 | name: Docker Build (tag) 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Check Out Repo 13 | uses: actions/checkout@v4 14 | 15 | - name: Login to Docker Hub 16 | uses: docker/login-action@v1 17 | with: 18 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 19 | password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 20 | 21 | - name: Set up Docker Buildx 22 | id: buildx 23 | uses: docker/setup-buildx-action@v1 24 | with: 25 | install: true 26 | 27 | - name: Extract repo name 28 | id: extract_repo_name 29 | shell: bash 30 | run: echo "##[set-output name=repo;]$(echo ${GITHUB_REPOSITORY#*/})" 31 | 32 | - name: Extract tag name 33 | id: extract_tag_name 34 | shell: bash 35 | run: echo "##[set-output name=tag;]$(echo ${GITHUB_REF##*/})" 36 | 37 | - name: Build and push 38 | id: docker_build 39 | uses: docker/build-push-action@v2 40 | with: 41 | context: ./ 42 | file: ./Dockerfile 43 | push: true 44 | tags: ${{ secrets.DOCKER_HUB_USERNAME }}/${{ steps.extract_repo_name.outputs.repo }}:v${{ steps.extract_tag_name.outputs.tag }} 45 | platforms: linux/amd64,linux/arm64 46 | 47 | - name: Image digest 48 | run: echo ${{ steps.docker_build.outputs.digest }} 49 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | env: 11 | GO_VERSION: "1.24" 12 | steps: 13 | - name: Set up Go ${{ env.GO_VERSION }} 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version: ${{ env.GO_VERSION }} 17 | 18 | - name: Check out code 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: golangci-lint 24 | uses: golangci/golangci-lint-action@v4 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - '*.*.*' 5 | 6 | name: Release 7 | jobs: 8 | goreleaser: 9 | name: Create Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: "1.24" 19 | 20 | - name: Run GoReleaser 21 | uses: goreleaser/goreleaser-action@v4 22 | with: 23 | args: release 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | go-version: ["1.24.x"] 12 | platform: [ubuntu-latest, macos-latest, windows-latest] 13 | runs-on: "${{ matrix.platform }}" 14 | env: 15 | CGO_ENABLED: 0 16 | steps: 17 | - name: Setup Go ${{ matrix.go-version }} 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: ${{ matrix.go-version }} 21 | 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Build 28 | run: go build -v -ldflags="-s -w" -trimpath -o ping_exporter 29 | 30 | - name: Test 31 | run: go test ./... -v -covermode=count 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | ping_exporter 4 | ping_exporter.exe 5 | artifacts 6 | .vscode/** 7 | 8 | # snap 9 | *.snap 10 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | dist: artifacts 2 | 3 | before: 4 | hooks: 5 | - go mod download 6 | - go mod tidy 7 | 8 | builds: 9 | - env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - linux 13 | - darwin 14 | - windows 15 | - freebsd 16 | goarch: 17 | - arm 18 | - arm64 19 | - mips 20 | - mipsle 21 | - mips64 22 | - mips64le 23 | - amd64 24 | gomips: 25 | - hardfloat 26 | - softfloat 27 | goarm: 28 | - "6" 29 | - "7" 30 | ignore: 31 | # pretty sure all 64bit MIPS chips have a FPU 32 | - { goarch: mips64, gomips: softfloat } 33 | - { goarch: mips64le, gomips: softfloat } 34 | flags: 35 | - -trimpath 36 | ldflags: 37 | - -s -w -X main.version={{ .Version }} 38 | binary: ping_exporter 39 | 40 | archives: 41 | - format: tar.gz 42 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if eq .Mips "softfloat" }}sf{{ end }}' 43 | format_overrides: 44 | - goos: windows 45 | format: zip 46 | 47 | snapshot: 48 | name_template: SNAPSHOT-{{ .ShortCommit }} 49 | 50 | nfpms: 51 | - vendor: Daniel Czerwonk 52 | homepage: "https://github.com/czerwonk/ping_exporter" 53 | maintainer: Daniel Czerwonk 54 | description: "Ping prometheus exporter" 55 | license: MIT 56 | section: net 57 | priority: extra 58 | 59 | file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if eq .Mips "softfloat" }}sf{{ end }}' 60 | 61 | formats: 62 | - rpm # TODO: create pre/post install/remove scripts 63 | - deb 64 | 65 | bindir: /usr/bin 66 | 67 | contents: 68 | # main systemd unit 69 | - src: dist/ping_exporter.service 70 | dst: /lib/systemd/system/ping_exporter.service 71 | 72 | # sample configuration 73 | - src: dist/ping_exporter.yaml 74 | dst: /etc/ping_exporter/config.yml 75 | type: config 76 | 77 | # systemd drop-ins, will be managed in postinstall and preremove script 78 | - src: dist/ping_exporter.d/systemd-*.conf 79 | dst: /usr/local/share/ping_exporter/ 80 | 81 | - dst: /run/systemd/system/ping_exporter.service.d 82 | type: dir 83 | 84 | overrides: 85 | deb: 86 | scripts: 87 | postinstall: dist/postinstall.sh 88 | preremove: dist/preremove.sh 89 | postremove: dist/postremove.sh 90 | 91 | changelog: 92 | sort: asc 93 | filters: 94 | exclude: 95 | - '^docs:' 96 | - '^test:' 97 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @czerwonk 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang as builder 2 | ADD . /go/ping_exporter/ 3 | WORKDIR /go/ping_exporter 4 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /go/bin/ping_exporter 5 | 6 | 7 | FROM alpine:latest 8 | ENV CONFIG_FILE "/config/config.yml" 9 | ENV CMD_FLAGS "" 10 | 11 | WORKDIR /app 12 | COPY --from=builder /go/bin/ping_exporter . 13 | RUN apk --no-cache add ca-certificates libcap && \ 14 | setcap cap_net_raw+ep /app/ping_exporter 15 | 16 | CMD ./ping_exporter --config.path $CONFIG_FILE $CMD_FLAGS 17 | EXPOSE 9427 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Daniel Czerwonk 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 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ping_exporter 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/czerwonk/ping_exporter)](https://goreportcard.com/report/github.com/czerwonk/ping_exporter) 3 | 4 | Prometheus exporter for ICMP echo requests using https://github.com/digineo/go-ping 5 | 6 | This is a simple server that scrapes go-ping stats and exports them via HTTP for 7 | Prometheus consumption. The go-ping library is build and maintained by Digineo GmbH. 8 | For more information check the [source code][go-ping]. 9 | 10 | [go-ping]: https://github.com/digineo/go-ping 11 | 12 | ## Getting Started 13 | 14 | ### Config file 15 | 16 | Targets can be specified in a YAML based config file: 17 | 18 | ```yaml 19 | targets: 20 | - 8.8.8.8 21 | - 8.8.4.4 22 | - 2001:4860:4860::8888 23 | - 2001:4860:4860::8844 24 | - google.com: 25 | asn: 15169 26 | 27 | dns: 28 | refresh: 2m15s 29 | nameserver: 1.1.1.1 30 | 31 | ping: 32 | interval: 2s 33 | timeout: 3s 34 | history-size: 42 35 | payload-size: 120 36 | 37 | options: 38 | disableIPv6: false 39 | ``` 40 | 41 | Note: domains are resolved (regularly) to their corresponding A and AAAA 42 | records (IPv4 and IPv6). By default, `ping_exporter` uses the system 43 | resolver to translate domain names to IP addresses. You can override the 44 | resolver address by specifying the `--dns.nameserver` flag when starting 45 | the binary, e.g. 46 | 47 | ```console 48 | $ # use Cloudflare's public DNS server 49 | $ ./ping_exporter --dns.nameserver=1.1.1.1:53 [other options] 50 | ``` 51 | 52 | The configuration file is watched via inotify. If the configuration is changed, 53 | ping_exporter will update the targets. To change any global options like the ping 54 | interval or history size, you must restart the exporter. 55 | 56 | ### Exported metrics 57 | 58 | - `ping_rtt_best_seconds`: Best round trip time in seconds 59 | - `ping_rtt_worst_seconds`: Worst round trip time in seconds 60 | - `ping_rtt_mean_seconds`: Mean round trip time in seconds 61 | - `ping_rtt_std_deviation_seconds`: Standard deviation in seconds 62 | - `ping_loss_ratio`: Packet loss as a value from 0.0 to 1.0 63 | 64 | Each metric has labels `ip` (the target's IP address), `ip_version` 65 | (4 or 6, corresponding to the IP version), and `target` (the target's 66 | name). 67 | 68 | Additionally, a `ping_up` metric reports whether the exporter 69 | is running (and in which version). 70 | 71 | ### Shell 72 | 73 | To run the exporter: 74 | 75 | ```console 76 | $ ./ping_exporter [options] target1 target2 ... 77 | ``` 78 | 79 | or 80 | 81 | ```console 82 | $ ./ping_exporter --config.path my-config-file [options] 83 | ``` 84 | 85 | Help on flags: 86 | 87 | ```console 88 | $ ./ping_exporter --help 89 | ``` 90 | 91 | Getting the results for testing via cURL: 92 | 93 | ```console 94 | $ curl http://localhost:9427/metrics 95 | ``` 96 | 97 | ### Running as non-root user 98 | 99 | On Linux systems `CAP_NET_RAW` is required to run `ping_exporter` as unpriviliged user. 100 | ```console 101 | # setcap cap_net_raw+ep /path/to/ping_exporter 102 | ``` 103 | 104 | When run through a rootless Docker implementation on Linux, the flag `--cap-add=CAP_NET_RAW` should be added to the `docker run` invocation. 105 | 106 | If being invoked via systemd, you can alternately just add the following 107 | settings to the service's unit file in the `[Service]` section: 108 | 109 | ```console 110 | CapabilityBoundingSet=CAP_NET_RAW 111 | AmbientCapabilities=CAP_NET_RAW 112 | ``` 113 | 114 | ## Docker 115 | 116 | https://hub.docker.com/r/czerwonk/ping_exporter 117 | 118 | To run the ping_exporter as a Docker container, run: 119 | 120 | ```console 121 | $ docker run -p 9427:9427 -v /path/to/config/directory:/config:ro --name ping_exporter czerwonk/ping_exporter 122 | ``` 123 | 124 | ## Kubernetes 125 | To run the ping_exporter in Kubernetes, you can use the supplied helm chart 126 | 127 | ### Prerequisites 128 | 129 | * Helm v3.0.0+ 130 | 131 | ### Installing the chart 132 | 133 | To install the chart with the release name `ping-exporter`: 134 | ```console 135 | $ helm repo add ping-exporter "https://raw.githubusercontent.com/czerwonk/ping_exporter/main/dist/charts/" 136 | "ping-exporter" has been added to your repositories 137 | 138 | $ helm repo update 139 | Hang tight while we grab the latest from your chart repositories... 140 | ...Successfully got an update from the "ping-exporter" chart repository 141 | Update Complete. ⎈Happy Helming!⎈ 142 | 143 | $ helm install ping-exporter ping-exporter/ping-exporter 144 | NAME: ping-exporter 145 | ... 146 | 147 | ``` 148 | 149 | ### General parameters 150 | | Key | Type | Default | Description | 151 | |-----|------|---------|-------------| 152 | | affinity | object | `{}` | [Affinity] | 153 | | args | list | `[]` | Add additional [command-line arguments] when running ping_exporter | 154 | | config | object | see [values.yaml] | Contains the contents of ping_exporter's [config file] | 155 | | fullnameOverride | string | `""` | String to fully override `"ping-exporter.fullname"` | 156 | | image.repository | string | `"czerwonk/ping_exporter"` | String to override the docker image repository | 157 | | image.pullPolicy | string | `"IfNotPresent"` | String to override the pullPolicy | 158 | | image.tag | string | `""` | Overrides the ping_exporter image tag whose default is the chart `appVersion` | 159 | | imagePullSecrets | list | `[]` | If defined, uses a secret to pull an image from a private Docker registry or repository | 160 | | ingress.enabled | bool | `false` | Enable an ingress resource for the ping_exporter | 161 | | ingress.className | string | `""` | Defines which ingress controller will implement the resource | 162 | | ingress.annotations | object | `{}` | Annotations to be added to the ingress resource | 163 | | ingress.hosts | list | `[{"host": "chart-example.local", "paths":[{"path": "/", "pathType": "ImplementationSpecific"}]}]` | Defines the [ingress] hosts and path to proxy | 164 | | ingress.tls | list | `[]` | Defines the secret(s) containing TLS certs for the [ingress] host | 165 | | nameOverride | string | `""` | Provide a name in place of `ping-exporter` | 166 | | podAnnotations | object | `{}` | Annotations to be added to ping_exporter pods | 167 | | podSecurityContext | object | `{}` | Sets the container-level security context | 168 | | replicaCount | number | `1` | Override the number of replicas running | 169 | | resources | object | `{}` | Defines the ping_exporter pod's resource cpu/memory limits and requests | 170 | | nodeSelector | object | `{}` | [Node selector] | 171 | | securityContext.capabilities | object | `{"add": ["CAP_NET_RAW"]}` | This object overrided the pod's security context capabilities | 172 | | service.type | string | `"ClusterIP"` | Sets the type of kubernetes service which is created for ping_exporter | 173 | | service.port | number | `9427` | Sets the port in which the kubernetes service will listen on and communicate with the ping_exporter pod | 174 | | service.annotations | object | `{}` | Annotations applied to the kubernetes service | 175 | | serviceAccount.create | bool | `true` | Create a service account for the application | 176 | | serviceAccount.annotations | object | `{}` | Annotations applied to created service account | 177 | | serviceAccount.name | string | `""` | Overrides the application's service account name which defaults to `"ping-exporter.fullname"` | 178 | | tolerations | list | `[]` | [Tolerations] | 179 | | testConnection.enabled | bool | `true` | Enable the test connection pod for the ping_exporter 180 | 181 | 182 | ## Changes from previous versions 183 | 184 | ### `ping_loss_ratio` vs `ping_loss_percent` 185 | 186 | Previous versions of the exporter reported packet loss via a metric named 187 | `ping_loss_percent`. This was somewhat misleading / wrong, because it never 188 | actually reported a percent value (it was always a value between 0 and 1). To 189 | make this more clear, and to match with [Prometheus best 190 | practices](https://prometheus.io/docs/practices/naming/#base-units), this 191 | metric has been renamed to `ping_loss_ratio` instead. 192 | 193 | If you had already been using an earlier version and want to continue to record 194 | this metric in Prometheus using the old name, this can be done using the 195 | `metric_relabel_configs` options in the Prometheus config, like so: 196 | 197 | ```console 198 | - job_name: "ping" 199 | static_configs: 200 | <...> 201 | metric_relabel_configs: 202 | - source_labels: [__name__] 203 | regex: "ping_loss_ratio" 204 | target_label: __name__ 205 | replacement: "ping_loss_percent" 206 | ``` 207 | 208 | ### Time units 209 | 210 | As per the recommendations for [Prometheus best 211 | practices](https://prometheus.io/docs/practices/naming/#base-units), the 212 | exporter reports time values in seconds by default. Previous versions 213 | defaulted to reporting milliseconds by default (with metric names ending in 214 | `_ms` instead of `_seconds`), so if you are upgrading from an older version, 215 | this may require some adjustment. 216 | 217 | It is possible to change the ping exporter to report times in milliseconds 218 | instead (this is not recommended, but may be useful for compatibility with 219 | older versions, etc). To do this, the `metrics.rttunit` command-line switch 220 | can be used: 221 | 222 | ```console 223 | $ # keep using seconds (default) 224 | $ ./ping_exporter --metrics.rttunit=s [other options] 225 | $ # use milliseconds instead 226 | $ ./ping_exporter --metrics.rttunit=ms [other options] 227 | $ # report both millis and seconds 228 | $ ./ping_exporter --metrics.rttunit=both [other options] 229 | ``` 230 | 231 | If you used the `ping_exporter` in the past, and want to migrate, start 232 | using `--metrics.rttunit=both` now. This gives you the opportunity to 233 | update all your alerts, dashboards, and other software depending on ms 234 | values to use proper scale (you "just" need to apply a factor of 1000 235 | on everything). When you're ready, you just need to switch to 236 | `--metrics.rttunit=s` (or just remove the command-line option entirely). 237 | 238 | ### Deprecated metrics 239 | 240 | Previous versions of this exporter provided an older form of the RTT metrics 241 | as: 242 | 243 | - `ping_rtt_ms`: Round trip times in millis 244 | 245 | This metric had a label `type` with one of the following values: 246 | 247 | - `best` denotes best round trip time 248 | - `worst` denotes worst round trip time 249 | - `mean` denotes mean round trip time 250 | - `std_dev` denotes standard deviation 251 | 252 | These metrics are no longer exported by default, but can be enabled for 253 | backwards compatibility using the `--metrics.deprecated` command-line flag: 254 | 255 | ```console 256 | $ # also export deprecated metrics 257 | $ ./ping_exporter --metrics.deprecated=enable [other options] 258 | $ # or omit deprecated metrics (default) 259 | $ ./ping_exporter --metrics.deprecated=disable [other options] 260 | ``` 261 | 262 | ## Contribute 263 | 264 | Simply fork and create a pull-request. We'll try to respond in a timely fashion. 265 | 266 | ## License 267 | 268 | MIT License, Copyright (c) 2018 269 | Philip Berndroth [pberndro](https://twitter.com/pberndro) 270 | Daniel Czerwonk [dan_nrw](https://twitter.com/dan_nrw) 271 | 272 | [Node selector]: https://kubernetes.io/docs/user-guide/node-selection/ 273 | [Ingress]: https://kubernetes.io/docs/concepts/services-networking/ingress/ 274 | [Tolerations]: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ 275 | [Affinity]: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ 276 | [command-line arguments]: https://github.com/czerwonk/ping_exporter#different-time-unit 277 | [config file]: https://github.com/czerwonk/ping_exporter#config-file 278 | [values.yaml]: https://github.com/czerwonk/ping_exporter/blob/main/dist/charts/ping-exporter/values.yaml 279 | -------------------------------------------------------------------------------- /collector.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package main 4 | 5 | import ( 6 | "strings" 7 | "sync" 8 | 9 | mon "github.com/digineo/go-ping/monitor" 10 | "github.com/prometheus/client_golang/prometheus" 11 | 12 | "github.com/czerwonk/ping_exporter/config" 13 | ) 14 | 15 | type pingCollector struct { 16 | monitor *mon.Monitor 17 | enableDeprecatedMetrics bool 18 | rttUnit rttUnit 19 | 20 | cfg *config.Config 21 | 22 | mutex sync.RWMutex 23 | 24 | customLabels *customLabelSet 25 | metrics map[string]*mon.Metrics 26 | 27 | rttDesc scaledMetrics 28 | bestDesc scaledMetrics 29 | worstDesc scaledMetrics 30 | meanDesc scaledMetrics 31 | stddevDesc scaledMetrics 32 | lossDesc *prometheus.Desc 33 | progDesc *prometheus.Desc 34 | } 35 | 36 | func NewPingCollector(enableDeprecatedMetrics bool, unit rttUnit, monitor *mon.Monitor, cfg *config.Config) *pingCollector { 37 | ret := &pingCollector{ 38 | monitor: monitor, 39 | enableDeprecatedMetrics: enableDeprecatedMetrics, 40 | rttUnit: unit, 41 | cfg: cfg, 42 | } 43 | ret.customLabels = newCustomLabelSet(cfg.Targets) 44 | ret.createDesc() 45 | return ret 46 | } 47 | 48 | func (p *pingCollector) UpdateConfig(cfg *config.Config) { 49 | p.mutex.Lock() 50 | defer p.mutex.Unlock() 51 | p.cfg.Targets = cfg.Targets 52 | p.customLabels = newCustomLabelSet(cfg.Targets) 53 | p.createDesc() 54 | } 55 | 56 | func (p *pingCollector) Describe(ch chan<- *prometheus.Desc) { 57 | if p.enableDeprecatedMetrics { 58 | p.rttDesc.Describe(ch) 59 | } 60 | p.bestDesc.Describe(ch) 61 | p.worstDesc.Describe(ch) 62 | p.meanDesc.Describe(ch) 63 | p.stddevDesc.Describe(ch) 64 | ch <- p.lossDesc 65 | ch <- p.progDesc 66 | } 67 | 68 | func (p *pingCollector) Collect(ch chan<- prometheus.Metric) { 69 | p.mutex.Lock() 70 | defer p.mutex.Unlock() 71 | 72 | if m := p.monitor.Export(); len(m) > 0 { 73 | p.metrics = m 74 | } 75 | 76 | ch <- prometheus.MustNewConstMetric(p.progDesc, prometheus.GaugeValue, 1) 77 | 78 | for target, metrics := range p.metrics { 79 | l := strings.SplitN(target, " ", 3) 80 | 81 | targetConfig := p.cfg.TargetConfigByAddr(l[0]) 82 | l = append(l, p.customLabels.labelValues(targetConfig)...) 83 | 84 | if metrics.PacketsSent > metrics.PacketsLost { 85 | if enableDeprecatedMetrics { 86 | p.rttDesc.Collect(ch, metrics.Best, append(l, "best")...) 87 | p.rttDesc.Collect(ch, metrics.Worst, append(l, "worst")...) 88 | p.rttDesc.Collect(ch, metrics.Mean, append(l, "mean")...) 89 | p.rttDesc.Collect(ch, metrics.StdDev, append(l, "std_dev")...) 90 | } 91 | 92 | p.bestDesc.Collect(ch, metrics.Best, l...) 93 | p.worstDesc.Collect(ch, metrics.Worst, l...) 94 | p.meanDesc.Collect(ch, metrics.Mean, l...) 95 | p.stddevDesc.Collect(ch, metrics.StdDev, l...) 96 | } 97 | 98 | loss := float64(metrics.PacketsLost) / float64(metrics.PacketsSent) 99 | ch <- prometheus.MustNewConstMetric(p.lossDesc, prometheus.GaugeValue, loss, l...) 100 | } 101 | } 102 | 103 | func (p *pingCollector) createDesc() { 104 | labelNames := []string{"target", "ip", "ip_version"} 105 | labelNames = append(labelNames, p.customLabels.labelNames()...) 106 | 107 | p.rttDesc = newScaledDesc("rtt", "Round trip time", p.rttUnit, append(labelNames, "type")) 108 | p.bestDesc = newScaledDesc("rtt_best", "Best round trip time", p.rttUnit, labelNames) 109 | p.worstDesc = newScaledDesc("rtt_worst", "Worst round trip time", p.rttUnit, labelNames) 110 | p.meanDesc = newScaledDesc("rtt_mean", "Mean round trip time", p.rttUnit, labelNames) 111 | p.stddevDesc = newScaledDesc("rtt_std_deviation", "Standard deviation", p.rttUnit, labelNames) 112 | p.lossDesc = newDesc("loss_ratio", "Packet loss from 0.0 to 1.0", labelNames, nil) 113 | p.progDesc = newDesc("up", "ping_exporter version", nil, prometheus.Labels{"version": version}) 114 | } 115 | 116 | func newDesc(name, help string, variableLabels []string, constLabels prometheus.Labels) *prometheus.Desc { 117 | return prometheus.NewDesc("ping_"+name, help, variableLabels, constLabels) 118 | } 119 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package config 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | 9 | yaml "gopkg.in/yaml.v2" 10 | ) 11 | 12 | // Config represents configuration for the exporter. 13 | type Config struct { 14 | Targets []TargetConfig `yaml:"targets"` 15 | 16 | Ping struct { 17 | Interval duration `yaml:"interval"` 18 | Timeout duration `yaml:"timeout"` 19 | History int `yaml:"history-size"` 20 | Size uint16 `yaml:"payload-size"` 21 | } `yaml:"ping"` 22 | 23 | DNS struct { 24 | Refresh duration `yaml:"refresh"` 25 | Nameserver string `yaml:"nameserver"` 26 | Timeout duration `yaml:"timeout"` 27 | } `yaml:"dns"` 28 | 29 | Options struct { 30 | DisableIPv6 bool `yaml:"disableIPv6"` // prohibits DNS resolved IPv6 addresses 31 | DisableIPv4 bool `yaml:"disableIPv4"` // prohibits DNS resolved IPv4 addresses 32 | } `yaml:"options"` 33 | } 34 | 35 | // FromYAML reads YAML from reader and unmarshals it to Config. 36 | func FromYAML(r io.Reader) (*Config, error) { 37 | c := &Config{} 38 | err := yaml.NewDecoder(r).Decode(c) 39 | if err != nil { 40 | return nil, fmt.Errorf("failed to decode YAML: %w", err) 41 | } 42 | 43 | return c, nil 44 | } 45 | 46 | // ToYAML encodes the given configuration to the writer as YAML 47 | func ToYAML(w io.Writer, cfg *Config) error { 48 | err := yaml.NewEncoder(w).Encode(cfg) 49 | if err != nil { 50 | return fmt.Errorf("failed to encode YAML: %w", err) 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func (cfg *Config) TargetConfigByAddr(addr string) TargetConfig { 57 | for _, t := range cfg.Targets { 58 | if t.Addr == addr { 59 | return t 60 | } 61 | } 62 | 63 | return TargetConfig{Addr: addr} 64 | } 65 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package config 4 | 5 | import ( 6 | "bytes" 7 | "os" 8 | "reflect" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestParseConfig(t *testing.T) { 14 | t.Parallel() 15 | 16 | f, err := os.Open("testdata/config_test.yml") 17 | if err != nil { 18 | t.Error("failed to open file", err) 19 | t.FailNow() 20 | } 21 | 22 | c, err := FromYAML(f) 23 | f.Close() 24 | if err != nil { 25 | t.Error("failed to parse", err) 26 | t.FailNow() 27 | } 28 | 29 | targets := []TargetConfig{ 30 | {Addr: "8.8.8.8"}, 31 | {Addr: "8.8.4.4"}, 32 | {Addr: "2001:4860:4860::8888"}, 33 | { 34 | Addr: "2001:4860:4860::8844", 35 | Labels: map[string]string{ 36 | "foo": "bar", 37 | }, 38 | }, 39 | } 40 | 41 | if !reflect.DeepEqual(targets, c.Targets) { 42 | t.Errorf("expected 4 targets (%v) but got %d (%v)", targets, len(c.Targets), c.Targets) 43 | t.FailNow() 44 | } 45 | 46 | if expected := 2*time.Minute + 15*time.Second; time.Duration(c.DNS.Refresh) != expected { 47 | t.Errorf("expected dns.refresh to be %v, got %v", expected, c.DNS.Refresh) 48 | } 49 | if expected := 5 * time.Second; time.Duration(c.DNS.Timeout) != expected { 50 | t.Errorf("expected dns.timeout to be %v, got %v", expected, c.DNS.Timeout) 51 | } 52 | if expected := "1.1.1.1"; c.DNS.Nameserver != expected { 53 | t.Errorf("expected dns.nameserver to be %q, got %q", expected, c.DNS.Nameserver) 54 | } 55 | 56 | if expected := 2 * time.Second; time.Duration(c.Ping.Interval) != expected { 57 | t.Errorf("expected ping.interval to be %v, got %v", expected, c.Ping.Interval) 58 | } 59 | if expected := 3 * time.Second; time.Duration(c.Ping.Timeout) != expected { 60 | t.Errorf("expected ping.timeout to be %v, got %v", expected, c.Ping.Timeout) 61 | } 62 | if expected := 42; c.Ping.History != expected { 63 | t.Errorf("expected ping.history-size to be %d, got %d", expected, c.Ping.History) 64 | } 65 | if expected := 120; c.Ping.Size != uint16(expected) { 66 | t.Errorf("expected ping.payload-size to be %d, got %d", expected, c.Ping.Size) 67 | } 68 | if expected := true; c.Options.DisableIPv6 != expected { 69 | t.Errorf("expected options.disable-ipv6 to be %v, got %v", expected, c.Options.DisableIPv6) 70 | } 71 | } 72 | 73 | func TestRoundtrip(t *testing.T) { 74 | f, err := os.Open("testdata/config_test.yml") 75 | if err != nil { 76 | t.Error("failed to open file", err) 77 | t.FailNow() 78 | } 79 | 80 | c, err := FromYAML(f) 81 | if err != nil { 82 | t.Error("failed to read file", err) 83 | t.FailNow() 84 | } 85 | 86 | buf := bytes.NewBuffer(nil) 87 | err = ToYAML(buf, c) 88 | if err != nil { 89 | t.Error("failed to encode config", err) 90 | t.FailNow() 91 | } 92 | 93 | after, err := FromYAML(buf) 94 | if err != nil { 95 | t.Error("failed to read config again", err) 96 | t.FailNow() 97 | } 98 | 99 | if !reflect.DeepEqual(c, after) { 100 | t.Error("config after Decode(Encode(cfg)) didn't match") 101 | t.FailNow() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /config/duration.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type duration time.Duration 9 | 10 | // UnmarshalYAML implements yaml.Unmarshaler interface. 11 | func (d *duration) UnmarshalYAML(unmashal func(interface{}) error) error { 12 | var s string 13 | if err := unmashal(&s); err != nil { 14 | return err 15 | } 16 | dur, err := time.ParseDuration(s) 17 | if err != nil { 18 | return fmt.Errorf("failed to decode duration: %w", err) 19 | } 20 | *d = duration(dur) 21 | 22 | return nil 23 | } 24 | 25 | func (d duration) MarshalYAML() (interface{}, error) { 26 | return d.Duration().String(), nil 27 | } 28 | 29 | // Duration is a convenience getter. 30 | func (d duration) Duration() time.Duration { 31 | return time.Duration(d) 32 | } 33 | 34 | // Set updates the underlying duration. 35 | func (d *duration) Set(dur time.Duration) { 36 | *d = duration(dur) 37 | } 38 | -------------------------------------------------------------------------------- /config/target.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type TargetConfig struct { 4 | Addr string 5 | Labels map[string]string 6 | } 7 | 8 | // UnmarshalYAML implements yaml.Unmarshaler interface. 9 | func (d *TargetConfig) UnmarshalYAML(unmashal func(interface{}) error) error { 10 | var s string 11 | if err := unmashal(&s); err == nil { 12 | d.Addr = s 13 | return nil 14 | } 15 | 16 | var x map[string]map[string]string 17 | if err := unmashal(&x); err != nil { 18 | return err 19 | } 20 | 21 | for addr, l := range x { 22 | d.Addr = addr 23 | d.Labels = l 24 | } 25 | 26 | return nil 27 | } 28 | 29 | func (d TargetConfig) MarshalYAML() (interface{}, error) { 30 | if d.Labels == nil { 31 | return d.Addr, nil 32 | } 33 | ret := make(map[string]map[string]string) 34 | ret[d.Addr] = d.Labels 35 | return ret, nil 36 | } 37 | -------------------------------------------------------------------------------- /config/testdata/config_test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | targets: 3 | - 8.8.8.8 4 | - 8.8.4.4 5 | - 2001:4860:4860::8888 6 | - "2001:4860:4860::8844": 7 | foo: "bar" 8 | 9 | dns: 10 | refresh: 2m15s 11 | nameserver: 1.1.1.1 12 | timeout: 5s 13 | 14 | ping: 15 | interval: 2s 16 | timeout: 3s 17 | history-size: 42 18 | payload-size: 120 19 | 20 | options: 21 | disableIPv6: true 22 | -------------------------------------------------------------------------------- /custom_labels.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/czerwonk/ping_exporter/config" 4 | 5 | type customLabelSet struct { 6 | names []string 7 | nameMap map[string]interface{} 8 | } 9 | 10 | func newCustomLabelSet(targets []config.TargetConfig) *customLabelSet { 11 | cl := &customLabelSet{ 12 | nameMap: make(map[string]interface{}), 13 | names: make([]string, 0), 14 | } 15 | 16 | for _, t := range targets { 17 | cl.addLabelsForTarget(&t) 18 | } 19 | 20 | return cl 21 | } 22 | 23 | func (cl *customLabelSet) addLabelsForTarget(t *config.TargetConfig) { 24 | if t.Labels == nil { 25 | return 26 | } 27 | 28 | for name := range t.Labels { 29 | cl.addLabel(name) 30 | } 31 | } 32 | 33 | func (cl *customLabelSet) addLabel(name string) { 34 | _, exists := cl.nameMap[name] 35 | if exists { 36 | return 37 | } 38 | 39 | cl.names = append(cl.names, name) 40 | cl.nameMap[name] = nil 41 | } 42 | 43 | func (cl *customLabelSet) labelNames() []string { 44 | return cl.names 45 | } 46 | 47 | func (cl *customLabelSet) labelValues(t config.TargetConfig) []string { 48 | values := make([]string, len(cl.names)) 49 | if t.Labels == nil { 50 | return values 51 | } 52 | 53 | for i, name := range cl.names { 54 | if value, isSet := t.Labels[name]; isSet { 55 | values[i] = value 56 | } 57 | } 58 | 59 | return values 60 | } 61 | -------------------------------------------------------------------------------- /dist/charts/index.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | entries: 3 | ping-exporter: 4 | - apiVersion: v2 5 | appVersion: 1.1.0 6 | created: "2024-02-28T11:45:46.282974652Z" 7 | description: Prometheus exporter for ICMP echo requests 8 | digest: b564d6150c79691c63189498cd7c117b3ece8ac8e33ee607f37354b4cfe128f9 9 | name: ping-exporter 10 | type: application 11 | urls: 12 | - ping-exporter-1.1.0.tgz 13 | version: 1.1.0 14 | - apiVersion: v2 15 | appVersion: 0.4.8 16 | created: "2024-02-28T11:45:46.282481775Z" 17 | description: Prometheus exporter for ICMP echo requests 18 | digest: e2dd5d6b44761c50dfbeff7721526778992d8ba0b9314af64c883cb6dab1d058 19 | name: ping-exporter 20 | type: application 21 | urls: 22 | - ping-exporter-1.0.0.tgz 23 | version: 1.0.0 24 | generated: "2024-02-28T11:45:46.281597437Z" 25 | -------------------------------------------------------------------------------- /dist/charts/ping-exporter-1.0.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/czerwonk/ping_exporter/4345fe6f6ee2124c47be9c718c9939d952706339/dist/charts/ping-exporter-1.0.0.tgz -------------------------------------------------------------------------------- /dist/charts/ping-exporter-1.1.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/czerwonk/ping_exporter/4345fe6f6ee2124c47be9c718c9939d952706339/dist/charts/ping-exporter-1.1.0.tgz -------------------------------------------------------------------------------- /dist/charts/ping-exporter/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /dist/charts/ping-exporter/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: ping-exporter 3 | description: Prometheus exporter for ICMP echo requests 4 | type: application 5 | version: 1.1.0 6 | appVersion: 1.1.0 7 | -------------------------------------------------------------------------------- /dist/charts/ping-exporter/files/prometheus.rules: -------------------------------------------------------------------------------- 1 | - alert: HighPingLossRatio 2 | expr: round(ping_loss_ratio * 100) > 5 3 | for: 5m 4 | labels: 5 | severity: warning 6 | annotations: 7 | summary: High ping loss ratio for {{ $labels.target }} 8 | description: "Ping loss ratio for {{ $labels.target }} is {{ $value }}%" 9 | - alert: HighPingRtt 10 | expr: round(ping_rtt_mean_seconds * 1000, 0.1) > 100 11 | for: 5m 12 | labels: 13 | severity: warning 14 | annotations: 15 | summary: High ping latency for {{ $labels.target }} 16 | description: "Ping latency for {{ $labels.target }} is {{ $value }} seconds" 17 | -------------------------------------------------------------------------------- /dist/charts/ping-exporter/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "ping_exporter.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "ping_exporter.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "ping_exporter.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "ping_exporter.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /dist/charts/ping-exporter/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "ping_exporter.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "ping_exporter.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "ping_exporter.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "ping_exporter.labels" -}} 37 | helm.sh/chart: {{ include "ping_exporter.chart" . }} 38 | {{ include "ping_exporter.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "ping_exporter.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "ping_exporter.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "ping_exporter.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "ping_exporter.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /dist/charts/ping-exporter/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "ping_exporter.fullname" . }} 5 | labels: 6 | {{- include "ping_exporter.labels" . | nindent 4 }} 7 | data: 8 | config.yml: | 9 | {{ toYaml .Values.config | indent 4 }} 10 | -------------------------------------------------------------------------------- /dist/charts/ping-exporter/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "ping_exporter.fullname" . }} 5 | labels: 6 | {{- include "ping_exporter.labels" . | nindent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | strategy: 10 | {{- toYaml .Values.strategy | nindent 4 }} 11 | selector: 12 | matchLabels: 13 | {{- include "ping_exporter.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | annotations: 17 | checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} 18 | {{- with .Values.podAnnotations }} 19 | {{- toYaml . | nindent 8 }} 20 | {{- end }} 21 | labels: 22 | {{- include "ping_exporter.selectorLabels" . | nindent 8 }} 23 | {{- if .Values.podLabels }} 24 | {{- toYaml .Values.podLabels | nindent 8 }} 25 | {{- end }} 26 | spec: 27 | {{- with .Values.imagePullSecrets }} 28 | imagePullSecrets: 29 | {{- toYaml . | nindent 8 }} 30 | {{- end }} 31 | serviceAccountName: {{ include "ping_exporter.serviceAccountName" . }} 32 | securityContext: 33 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 34 | volumes: 35 | - name: config 36 | configMap: 37 | name: {{ include "ping_exporter.fullname" . }} 38 | containers: 39 | - name: {{ .Chart.Name }} 40 | volumeMounts: 41 | - name: config 42 | mountPath: "/config" 43 | securityContext: 44 | {{- toYaml .Values.securityContext | nindent 12 }} 45 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default (print "v" .Chart.AppVersion) }}" 46 | imagePullPolicy: {{ .Values.image.pullPolicy }} 47 | command: ["/app/ping_exporter"] 48 | args: 49 | - "--config.path=/config/config.yml" 50 | {{- range .Values.args }} 51 | - {{ . | quote -}} 52 | {{ end }} 53 | ports: 54 | - name: http 55 | containerPort: {{ .Values.service.port }} 56 | protocol: TCP 57 | livenessProbe: 58 | httpGet: 59 | path: / 60 | port: http 61 | readinessProbe: 62 | httpGet: 63 | path: / 64 | port: http 65 | resources: 66 | {{- toYaml .Values.resources | nindent 12 }} 67 | {{- with .Values.nodeSelector }} 68 | nodeSelector: 69 | {{- toYaml . | nindent 8 }} 70 | {{- end }} 71 | {{- with .Values.affinity }} 72 | affinity: 73 | {{- toYaml . | nindent 8 }} 74 | {{- end }} 75 | {{- with .Values.tolerations }} 76 | tolerations: 77 | {{- toYaml . | nindent 8 }} 78 | {{- end }} 79 | -------------------------------------------------------------------------------- /dist/charts/ping-exporter/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "ping_exporter.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "ping_exporter.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /dist/charts/ping-exporter/templates/prometheusrule.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.prometheusRules.enabled }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: PrometheusRule 4 | metadata: 5 | labels: 6 | prometheus: service-prometheus 7 | role: alert-rules 8 | name: {{ include "ping_exporter.fullname" . }} 9 | spec: 10 | groups: 11 | - name: ping_exporter.rules 12 | rules: 13 | {{ .Files.Get "files/prometheus.rules" }} 14 | {{- end }} 15 | -------------------------------------------------------------------------------- /dist/charts/ping-exporter/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "ping_exporter.fullname" . }} 5 | {{- with .Values.service.annotations }} 6 | annotations: 7 | {{- toYaml . | nindent 4 }} 8 | {{- end }} 9 | labels: 10 | {{- include "ping_exporter.labels" . | nindent 4 }} 11 | spec: 12 | type: {{ .Values.service.type }} 13 | ports: 14 | - port: {{ .Values.service.port }} 15 | targetPort: http 16 | protocol: TCP 17 | name: http 18 | selector: 19 | {{- include "ping_exporter.selectorLabels" . | nindent 4 }} 20 | -------------------------------------------------------------------------------- /dist/charts/ping-exporter/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "ping_exporter.serviceAccountName" . }} 6 | labels: 7 | {{- include "ping_exporter.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /dist/charts/ping-exporter/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceMonitor.enabled }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | name: {{ include "ping_exporter.fullname" . }} 6 | labels: 7 | {{- include "ping_exporter.labels" . | nindent 4 }} 8 | spec: 9 | selector: 10 | matchLabels: 11 | {{- include "ping_exporter.selectorLabels" . | nindent 6 }} 12 | endpoints: 13 | - port: http 14 | interval: 60s 15 | relabelings: 16 | - action: labeldrop 17 | regex: pod 18 | sourceLabels: [] 19 | - action: labeldrop 20 | regex: namespace 21 | sourceLabels: [] 22 | - action: labeldrop 23 | regex: instance 24 | sourceLabels: [] 25 | - action: labeldrop 26 | regex: job 27 | sourceLabels: [] 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /dist/charts/ping-exporter/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | 2 | {{- if .Values.testConnection.enabled -}} 3 | apiVersion: v1 4 | kind: Pod 5 | metadata: 6 | name: "{{ include "ping_exporter.fullname" . }}-test-connection" 7 | labels: 8 | {{- include "ping_exporter.labels" . | nindent 4 }} 9 | annotations: 10 | "helm.sh/hook": test 11 | spec: 12 | containers: 13 | - name: wget 14 | image: busybox 15 | command: ['wget'] 16 | args: ['{{ include "ping_exporter.fullname" . }}:{{ .Values.service.port }}'] 17 | restartPolicy: Never 18 | {{- with .Values.nodeSelector }} 19 | nodeSelector: 20 | {{- toYaml . | nindent 4 }} 21 | {{- end }} 22 | {{- with .Values.affinity }} 23 | affinity: 24 | {{- toYaml . | nindent 4 }} 25 | {{- end }} 26 | {{- with .Values.tolerations }} 27 | tolerations: 28 | {{- toYaml . | nindent 4 }} 29 | {{- end }} 30 | {{- end }} -------------------------------------------------------------------------------- /dist/charts/ping-exporter/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | 3 | image: 4 | repository: czerwonk/ping_exporter 5 | pullPolicy: IfNotPresent 6 | # Overrides the image tag whose default is the chart appVersion. 7 | tag: "" 8 | 9 | imagePullSecrets: [] 10 | # - name: my-image-pull-secret 11 | nameOverride: "" 12 | fullnameOverride: "" 13 | 14 | serviceAccount: 15 | # Specifies whether a service account should be created 16 | create: true 17 | # Annotations to add to the service account 18 | annotations: {} 19 | # The name of the service account to use. 20 | # If not set and create is true, a name is generated using the fullname template 21 | name: "" 22 | 23 | podAnnotations: {} 24 | 25 | podLabels: {} 26 | 27 | # Rollout strategy, could be "Recreate" or "RollingUpdate" 28 | strategy: 29 | type: RollingUpdate 30 | 31 | podSecurityContext: {} 32 | # fsGroup: 2000 33 | 34 | securityContext: 35 | capabilities: 36 | add: 37 | - NET_RAW 38 | # drop: 39 | # - ALL 40 | # readOnlyRootFilesystem: true 41 | # runAsNonRoot: true 42 | # runAsUser: 1000 43 | 44 | service: 45 | type: ClusterIP 46 | port: 9427 47 | annotations: {} 48 | # prometheus.io/scrape: "true" 49 | # prometheus.io/port: "9427" 50 | 51 | ingress: 52 | enabled: false 53 | className: "" 54 | annotations: {} 55 | # kubernetes.io/ingress.class: nginx 56 | # kubernetes.io/tls-acme: "true" 57 | hosts: 58 | - host: chart-example.local 59 | paths: 60 | - path: / 61 | pathType: ImplementationSpecific 62 | tls: [] 63 | # - secretName: chart-example-tls 64 | # hosts: 65 | # - chart-example.local 66 | 67 | # If you do want to specify resources, uncomment the following lines, adjust 68 | # them as necessary, and remove the curly braces after 'resources:'. 69 | resources: {} 70 | # limits: 71 | # cpu: 100m 72 | # memory: 128Mi 73 | # requests: 74 | # cpu: 100m 75 | # memory: 128Mi 76 | 77 | nodeSelector: {} 78 | 79 | tolerations: [] 80 | 81 | affinity: {} 82 | 83 | # Additional ping_exporter command line arguments. 84 | args: [] 85 | 86 | # The 'config' block contains the contents of the YAML based config file. 87 | config: 88 | targets: 89 | - 8.8.8.8 90 | - 8.8.4.4 91 | - 2001:4860:4860::8888 92 | - 2001:4860:4860::8844 93 | - google.com 94 | 95 | dns: 96 | refresh: 2m15s 97 | nameserver: 1.1.1.1 98 | 99 | ping: 100 | interval: 2s 101 | timeout: 3s 102 | history-size: 42 103 | payload-size: 120 104 | 105 | # Create a serviceMonitor resource to be consumed by Prometheus Operator 106 | serviceMonitor: 107 | enabled: false 108 | 109 | # Create basic Prometheus alerting rules 110 | prometheusRules: 111 | enabled: false 112 | 113 | testConnection: 114 | enabled: true 115 | -------------------------------------------------------------------------------- /dist/ping_exporter.d/systemd-233.conf: -------------------------------------------------------------------------------- 1 | # hardening for systemd 233+ 2 | [Service] 3 | RestrictNamespaces=yes 4 | -------------------------------------------------------------------------------- /dist/ping_exporter.d/systemd-235.conf: -------------------------------------------------------------------------------- 1 | # hardening for systemd 235+ 2 | [Service] 3 | LockPersonality=yes 4 | -------------------------------------------------------------------------------- /dist/ping_exporter.d/systemd-242.conf: -------------------------------------------------------------------------------- 1 | # hardening for systemd 242+ 2 | [Service] 3 | NoNewPrivileges=yes 4 | ProtectHostname=true 5 | -------------------------------------------------------------------------------- /dist/ping_exporter.d/systemd-245.conf: -------------------------------------------------------------------------------- 1 | # hardening for systemd 245+ 2 | [Service] 3 | ProtectClock=true 4 | RestrictSUIDSGID=yes 5 | -------------------------------------------------------------------------------- /dist/ping_exporter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Ping Exporter 3 | After=network.target 4 | 5 | [Service] 6 | User=ping_exporter 7 | ExecStart=/usr/bin/ping_exporter --config.path=/etc/ping_exporter/config.yml 8 | 9 | # This unit assumes systemd 232, present in EdgeOS 2.0.0 10 | # (a derivative of Vyatta/Debian 9). 11 | # 12 | # If the ping_exporter was installed on system with a newer systemd 13 | # version, you'll find additional drop-ins in ping_exporter.d/. 14 | 15 | CapabilityBoundingSet=CAP_NET_RAW 16 | AmbientCapabilities=CAP_NET_RAW 17 | PrivateDevices=true 18 | PrivateTmp=yes 19 | ProtectControlGroups=true 20 | ProtectKernelModules=yes 21 | ProtectKernelTunables=true 22 | ProtectSystem=strict 23 | ProtectHome=true 24 | DevicePolicy=closed 25 | RestrictRealtime=yes 26 | MemoryDenyWriteExecute=yes 27 | 28 | [Install] 29 | WantedBy=default.target 30 | -------------------------------------------------------------------------------- /dist/ping_exporter.yaml: -------------------------------------------------------------------------------- 1 | # List of target hosts (IP addresses or host names) to ping. 2 | targets: 3 | - 127.0.0.1 4 | 5 | dns: 6 | # enforce a specific DNS server for host name lookups (optional, 7 | # defaults to system resolver) 8 | #nameserver: 1.1.1.1 9 | 10 | # refresh interval for host name (optional) 11 | #refresh: 2m45s 12 | 13 | ping: 14 | interval: 2s # How often to ping a target? 15 | timeout: 3s # Timeout for a single ICMP Echo Request 16 | history-size: 42 # number of results to keep per target 17 | payload-size: 56 # message size for ICMP Echo Requests 18 | -------------------------------------------------------------------------------- /dist/postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | groupadd --system ping_exporter || true 4 | useradd --system -d /nonexistent -s /usr/sbin/nologin -g ping_exporter ping_exporter || true 5 | 6 | chown ping_exporter /etc/ping_exporter/config.yml 7 | 8 | current_systemd_version=$(dpkg-query --showformat='${Version}' --show systemd) 9 | 10 | for v in 233 235 242 245; do 11 | if dpkg --compare-versions "$current_systemd_version" ge "$v"; then 12 | cp /usr/local/share/ping_exporter/systemd-$v.conf /run/systemd/system/ping_exporter.service.d/ 13 | fi 14 | done 15 | 16 | systemctl daemon-reload 17 | systemctl enable ping_exporter 18 | systemctl restart ping_exporter 19 | -------------------------------------------------------------------------------- /dist/postremove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$1" != "remove" ]; then 4 | exit 0 5 | fi 6 | 7 | systemctl daemon-reload 8 | userdel ping_exporter || true 9 | groupdel ping_exporter 2>/dev/null || true 10 | -------------------------------------------------------------------------------- /dist/preremove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$1" != "remove" ]; then 4 | exit 0 5 | fi 6 | 7 | systemctl disable ping_exporter || true 8 | systemctl stop ping_exporter || true 9 | 10 | for v in 233 235 242 245; do 11 | rm -f /run/systemd/system/ping_exporter.service.d/systemd-$v.conf 12 | done 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/czerwonk/ping_exporter 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/alecthomas/kingpin/v2 v2.4.0 7 | github.com/digineo/go-ping v1.1.0 8 | github.com/prometheus/client_golang v1.21.0 9 | github.com/sirupsen/logrus v1.9.3 10 | gopkg.in/fsnotify.v1 v1.4.7 11 | gopkg.in/yaml.v2 v2.4.0 12 | tailscale.com v1.80.2 13 | ) 14 | 15 | require ( 16 | filippo.io/edwards25519 v1.1.0 // indirect 17 | github.com/akutz/memconn v0.1.0 // indirect 18 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 19 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect 20 | github.com/beorn7/perks v1.0.1 // indirect 21 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 22 | github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect 23 | github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0 // indirect 24 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 25 | github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 // indirect 26 | github.com/google/go-cmp v0.6.0 // indirect 27 | github.com/hdevalence/ed25519consensus v0.2.0 // indirect 28 | github.com/jsimonetti/rtnetlink v1.4.0 // indirect 29 | github.com/klauspost/compress v1.17.11 // indirect 30 | github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect 31 | github.com/mdlayher/socket v0.5.0 // indirect 32 | github.com/mitchellh/go-ps v1.0.0 // indirect 33 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 34 | github.com/prometheus/client_model v0.6.1 // indirect 35 | github.com/prometheus/common v0.62.0 // indirect 36 | github.com/prometheus/procfs v0.15.1 // indirect 37 | github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect 38 | github.com/x448/float16 v0.8.4 // indirect 39 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 40 | go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect 41 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect 42 | golang.org/x/crypto v0.36.0 // indirect 43 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect 44 | golang.org/x/net v0.38.0 // indirect 45 | golang.org/x/sync v0.12.0 // indirect 46 | golang.org/x/sys v0.31.0 // indirect 47 | golang.org/x/text v0.23.0 // indirect 48 | golang.zx2c4.com/wireguard/windows v0.5.3 // indirect 49 | google.golang.org/protobuf v1.36.1 // indirect 50 | ) 51 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= 4 | github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= 5 | github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= 6 | github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= 7 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= 8 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 9 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= 10 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= 11 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 12 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 13 | github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= 14 | github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 15 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 16 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 17 | github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= 18 | github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= 19 | github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= 20 | github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= 21 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 24 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk= 26 | github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ= 27 | github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0 h1:OT/LKmj81wMymnWXaKaKBR9n1vPlu+GC0VVKaZP6kzs= 28 | github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0/go.mod h1:DmqdumeAKGQNU5E8MN0ruT5ZGx8l/WbAsMbXCXcSEts= 29 | github.com/digineo/go-ping v1.1.0 h1:HXZPBw8/Zk+tFuHrHejBTLopcEkqK4FNn1ocqKo6xhw= 30 | github.com/digineo/go-ping v1.1.0/go.mod h1:rVhwm0cbn6i20vX/MBmo4OoxOvAW/6JiIf+2Oln8n0M= 31 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 32 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 33 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 34 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 35 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 36 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 37 | github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc= 38 | github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= 39 | github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 h1:KbX3Z3CgiYlbaavUq3Cj9/MjpO+88S7/AGXzynVDv84= 40 | github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= 41 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 42 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 43 | github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= 44 | github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= 45 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 46 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 47 | github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= 48 | github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 49 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 50 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 51 | github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= 52 | github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= 53 | github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= 54 | github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= 55 | github.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A= 56 | github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE= 57 | github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I= 58 | github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E= 59 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 60 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 61 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 62 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 63 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 64 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 65 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 66 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 67 | github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= 68 | github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= 69 | github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg= 70 | github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o= 71 | github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= 72 | github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= 73 | github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= 74 | github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= 75 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 76 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 77 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 78 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 79 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 80 | github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= 81 | github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= 82 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 83 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 84 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 85 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 86 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 87 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 88 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 89 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 90 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 91 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 92 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 93 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 94 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 95 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 96 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 97 | github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= 98 | github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= 99 | github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU= 100 | github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= 101 | github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19 h1:BcEJP2ewTIK2ZCsqgl6YGpuO6+oKqqag5HHb7ehljKw= 102 | github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= 103 | github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= 104 | github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 105 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 106 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 107 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= 108 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= 109 | go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= 110 | go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= 111 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= 112 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= 113 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 114 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 115 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= 116 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= 117 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 118 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 119 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 120 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 121 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 122 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 123 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 124 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 125 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 126 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 127 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 128 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 129 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 130 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= 131 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= 132 | golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= 133 | golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= 134 | google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= 135 | google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 136 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 137 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 138 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 139 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 140 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 141 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 142 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 143 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 144 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 145 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 146 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 147 | gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8= 148 | gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= 149 | tailscale.com v1.80.2 h1:MA/AvAyWakq01E1MS6SwKEp2VEFE5CoXAAguwrnbF5g= 150 | tailscale.com v1.80.2/go.mod h1:HTOFVeo5RY0qBl5Uy+LXHwgp0PLXgVSfgqWI34gSrPA= 151 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package main 4 | 5 | import log "github.com/sirupsen/logrus" 6 | 7 | func setLogLevel(l string) { 8 | switch l { 9 | case "debug": 10 | log.SetLevel(log.DebugLevel) 11 | case "error": 12 | log.SetLevel(log.ErrorLevel) 13 | case "fatal": 14 | log.SetLevel(log.FatalLevel) 15 | case "warn": 16 | log.SetLevel(log.WarnLevel) 17 | default: 18 | log.SetLevel(log.InfoLevel) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "fmt" 10 | "net" 11 | "net/http" 12 | "os" 13 | "strings" 14 | "sync" 15 | "time" 16 | 17 | "github.com/digineo/go-ping" 18 | mon "github.com/digineo/go-ping/monitor" 19 | 20 | "github.com/czerwonk/ping_exporter/config" 21 | 22 | "github.com/alecthomas/kingpin/v2" 23 | "github.com/prometheus/client_golang/prometheus" 24 | "github.com/prometheus/client_golang/prometheus/promhttp" 25 | log "github.com/sirupsen/logrus" 26 | inotify "gopkg.in/fsnotify.v1" 27 | ) 28 | 29 | const version string = "1.1.3" 30 | 31 | var ( 32 | showVersion = kingpin.Flag("version", "Print version information").Default().Bool() 33 | listenAddress = kingpin.Flag("web.listen-address", "Address on which to expose metrics and web interface").Default(":9427").String() 34 | metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics").Default("/metrics").String() 35 | metricsToken = kingpin.Flag("web.token", "Token (in http request headers of queries) which to expose metrics").Default("").String() 36 | serverUseTLS = kingpin.Flag("web.tls.enabled", "Enable TLS for web server, default is false").Default().Bool() 37 | serverTlsCertFile = kingpin.Flag("web.tls.cert-file", "The certificate file for the web server").Default("").String() 38 | serverTlsKeyFile = kingpin.Flag("web.tls.key-file", "The key file for the web server").Default("").String() 39 | serverMutualAuthEnabled = kingpin.Flag("web.tls.mutual-auth-enabled", "Enable TLS client mutual authentication, default is false").Default().Bool() 40 | serverTlsCAFile = kingpin.Flag("web.tls.ca-file", "The certificate authority file for client's certificate verification").Default("").String() 41 | configFile = kingpin.Flag("config.path", "Path to config file").Default("").String() 42 | pingInterval = kingpin.Flag("ping.interval", "Interval for ICMP echo requests").Default("5s").Duration() 43 | pingTimeout = kingpin.Flag("ping.timeout", "Timeout for ICMP echo request").Default("4s").Duration() 44 | pingSize = kingpin.Flag("ping.size", "Payload size for ICMP echo requests").Default("56").Uint16() 45 | historySize = kingpin.Flag("ping.history-size", "Number of results to remember per target").Default("10").Int() 46 | dnsRefresh = kingpin.Flag("dns.refresh", "Interval for refreshing DNS records and updating targets accordingly (0 if disabled)").Default("1m").Duration() 47 | dnsNameServer = kingpin.Flag("dns.nameserver", "DNS server used to resolve hostname of targets").Default("").String() 48 | dnsLookupTimeout = kingpin.Flag("dns.timeout", "Timeout for DNS resolution").Default("0s").Duration() 49 | disableIPv6 = kingpin.Flag("options.disable-ipv6", "Disable DNS from resolving IPv6 AAAA records").Default().Bool() 50 | disableIPv4 = kingpin.Flag("options.disable-ipv4", "Disable DNS from resolving IPv4 A records").Default().Bool() 51 | logLevel = kingpin.Flag("log.level", "Only log messages with the given severity or above. Valid levels: [debug, info, warn, error, fatal]").Default("info").String() 52 | targetFlag = kingpin.Arg("targets", "A list of targets to ping").Strings() 53 | 54 | tailnet = kingpin.Flag("ts.tailnet", "tailnet name").String() 55 | ) 56 | 57 | var ( 58 | enableDeprecatedMetrics = true // default may change in future 59 | deprecatedMetrics = kingpin.Flag("metrics.deprecated", "Enable or disable deprecated metrics (`ping_rtt_ms{type=best|worst|mean|std_dev}`). Valid choices: [enable, disable]").Default("disable").String() 60 | 61 | rttMetricsScale = rttInMills // might change in future 62 | rttMode = kingpin.Flag("metrics.rttunit", "Export ping results as either seconds (default), or milliseconds (deprecated), or both (for migrations). Valid choices: [s, ms, both]").Default("s").String() 63 | desiredTargets *targets 64 | ) 65 | 66 | func main() { 67 | desiredTargets = &targets{} 68 | kingpin.Parse() 69 | 70 | if len(*tailnet) > 0 { 71 | tsDiscover() 72 | } 73 | 74 | if *showVersion { 75 | printVersion() 76 | os.Exit(0) 77 | } 78 | 79 | setLogLevel(*logLevel) 80 | log.SetReportCaller(true) 81 | 82 | switch *deprecatedMetrics { 83 | case "enable": 84 | enableDeprecatedMetrics = true 85 | case "disable": 86 | enableDeprecatedMetrics = false 87 | default: 88 | kingpin.FatalUsage("metrics.deprecated must be `enable` or `disable`") 89 | } 90 | 91 | if rttMetricsScale = rttUnitFromString(*rttMode); rttMetricsScale == rttInvalid { 92 | kingpin.FatalUsage("metrics.rttunit must be `ms` for millis, or `s` for seconds, or `both`") 93 | } 94 | log.Infof("rtt units: %#v", rttMetricsScale) 95 | 96 | if mpath := *metricsPath; mpath == "" { 97 | log.Warnln("web.telemetry-path is empty, correcting to `/metrics`") 98 | mpath = "/metrics" 99 | metricsPath = &mpath 100 | } else if mpath[0] != '/' { 101 | mpath = "/" + mpath 102 | metricsPath = &mpath 103 | } 104 | 105 | cfg, err := loadConfig() 106 | if err != nil { 107 | kingpin.FatalUsage("could not load config.path: %v", err) 108 | } 109 | 110 | if cfg.Ping.History < 1 { 111 | kingpin.FatalUsage("ping.history-size must be greater than 0") 112 | } 113 | 114 | if cfg.Ping.Size > 65500 { 115 | kingpin.FatalUsage("ping.size must be between 0 and 65500") 116 | } 117 | 118 | if len(cfg.Targets) == 0 { 119 | kingpin.FatalUsage("No targets specified") 120 | } 121 | 122 | resolver := setupResolver(cfg) 123 | 124 | m, err := startMonitor(cfg, resolver) 125 | if err != nil { 126 | log.Errorln(err) 127 | os.Exit(2) 128 | } 129 | 130 | collector := NewPingCollector(enableDeprecatedMetrics, rttMetricsScale, m, cfg) 131 | go watchConfig(desiredTargets, resolver, m, collector) 132 | 133 | startServer(collector) 134 | } 135 | 136 | func printVersion() { 137 | fmt.Println("ping-exporter") 138 | fmt.Printf("Version: %s\n", version) 139 | fmt.Println("Author(s): Philip Berndroth, Daniel Czerwonk") 140 | fmt.Println("Metric exporter for go-icmp") 141 | } 142 | 143 | func startMonitor(cfg *config.Config, resolver *net.Resolver) (*mon.Monitor, error) { 144 | var bind4, bind6 string 145 | if ln, err := net.Listen("tcp4", "127.0.0.1:0"); err == nil { 146 | // ipv4 enabled 147 | ln.Close() 148 | bind4 = "0.0.0.0" 149 | } 150 | if ln, err := net.Listen("tcp6", "[::1]:0"); err == nil { 151 | // ipv6 enabled 152 | ln.Close() 153 | bind6 = "::" 154 | } 155 | pinger, err := ping.New(bind4, bind6) 156 | if err != nil { 157 | return nil, fmt.Errorf("cannot start monitoring: %w", err) 158 | } 159 | 160 | if pinger.PayloadSize() != cfg.Ping.Size { 161 | pinger.SetPayloadSize(cfg.Ping.Size) 162 | } 163 | 164 | monitor := mon.New(pinger, 165 | cfg.Ping.Interval.Duration(), 166 | cfg.Ping.Timeout.Duration()) 167 | monitor.HistorySize = cfg.Ping.History 168 | 169 | err = upsertTargets(desiredTargets, resolver, cfg, monitor) 170 | if err != nil { 171 | log.Fatalln(err) 172 | } 173 | 174 | go startDNSAutoRefresh(cfg.DNS.Refresh.Duration(), desiredTargets, monitor, cfg) 175 | return monitor, nil 176 | } 177 | 178 | func upsertTargets(globalTargets *targets, resolver *net.Resolver, cfg *config.Config, monitor *mon.Monitor) error { 179 | oldTargets := globalTargets.Targets() 180 | newTargets := make([]*target, len(cfg.Targets)) 181 | var wg sync.WaitGroup 182 | for i, t := range cfg.Targets { 183 | newTarget := globalTargets.Get(t.Addr) 184 | if newTarget == nil { 185 | newTarget = &target{ 186 | host: t.Addr, 187 | addresses: make([]net.IPAddr, 0), 188 | delay: time.Duration(10*i) * time.Millisecond, 189 | resolver: resolver, 190 | } 191 | } 192 | 193 | newTargets[i] = newTarget 194 | 195 | wg.Add(1) 196 | go func() { 197 | err := newTarget.addOrUpdateMonitor(monitor, targetOpts{ 198 | disableIPv4: cfg.Options.DisableIPv4, 199 | disableIPv6: cfg.Options.DisableIPv6, 200 | }, cfg) 201 | if err != nil { 202 | log.Errorf("failed to setup target: %v", err) 203 | } 204 | wg.Done() 205 | }() 206 | } 207 | wg.Wait() 208 | globalTargets.SetTargets(newTargets) 209 | 210 | removed := removedTargets(oldTargets, globalTargets) 211 | for _, removedTarget := range removed { 212 | log.Infof("remove target: %s", removedTarget.host) 213 | removedTarget.removeFromMonitor(monitor) 214 | } 215 | return nil 216 | } 217 | 218 | func watchConfig(globalTargets *targets, resolver *net.Resolver, monitor *mon.Monitor, collector *pingCollector) { 219 | watcher, err := inotify.NewWatcher() 220 | if err != nil { 221 | log.Fatalf("unable to create file watcher: %v", err) 222 | } 223 | 224 | err = watcher.Add(*configFile) 225 | if err != nil { 226 | log.Fatalf("unable to watch file: %v", err) 227 | } 228 | for { 229 | select { 230 | case event := <-watcher.Events: 231 | log.Debugf("Got file inotify event: %s", event) 232 | // If the file is removed, the inotify watcher will lose track of the file. Add it again. 233 | if event.Op == inotify.Remove { 234 | if err = watcher.Add(*configFile); err != nil { 235 | log.Fatalf("failed to renew watch for file: %v", err) 236 | } 237 | } 238 | cfg, err := loadConfig() 239 | if err != nil { 240 | log.Errorf("unable to load config: %v", err) 241 | continue 242 | } 243 | // We get zero targets if the file was truncated. This happens if an automation tool rewrites 244 | // the complete file, instead of alternating only parts of it. 245 | if len(cfg.Targets) == 0 { 246 | continue 247 | } 248 | log.Infof("reloading config file %s", *configFile) 249 | if err := upsertTargets(globalTargets, resolver, cfg, monitor); err != nil { 250 | log.Errorf("failed to reload config: %v", err) 251 | continue 252 | } 253 | collector.UpdateConfig(cfg) 254 | case err := <-watcher.Errors: 255 | log.Errorf("watching file failed: %v", err) 256 | } 257 | } 258 | } 259 | 260 | func removedTargets(old []*target, new *targets) []*target { 261 | var ret []*target 262 | for _, oldTarget := range old { 263 | if !new.Contains(oldTarget) { 264 | ret = append(ret, oldTarget) 265 | } 266 | } 267 | return ret 268 | } 269 | 270 | func startDNSAutoRefresh(interval time.Duration, tar *targets, monitor *mon.Monitor, cfg *config.Config) { 271 | if interval <= 0 { 272 | return 273 | } 274 | 275 | for range time.NewTicker(interval).C { 276 | refreshDNS(tar, monitor, cfg) 277 | } 278 | } 279 | 280 | func refreshDNS(tar *targets, monitor *mon.Monitor, cfg *config.Config) { 281 | log.Infoln("refreshing DNS") 282 | for _, t := range tar.Targets() { 283 | go func(ta *target) { 284 | err := ta.addOrUpdateMonitor(monitor, targetOpts{ 285 | disableIPv4: cfg.Options.DisableIPv4, 286 | disableIPv6: cfg.Options.DisableIPv6, 287 | }, cfg) 288 | if err != nil { 289 | log.Errorf("could not refresh dns: %v", err) 290 | } 291 | }(t) 292 | } 293 | } 294 | 295 | func startServer(collector *pingCollector) { 296 | var err error 297 | log.Infof("Starting ping exporter (Version: %s)", version) 298 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 299 | if !hasValidToken(r, w) { 300 | return 301 | } 302 | 303 | fmt.Fprintf(w, indexHTML, *metricsPath) 304 | }) 305 | 306 | reg := prometheus.NewRegistry() 307 | reg.MustRegister(collector) 308 | 309 | l := log.New() 310 | l.Level = log.ErrorLevel 311 | 312 | h := promhttp.HandlerFor(reg, promhttp.HandlerOpts{ 313 | ErrorLog: l, 314 | ErrorHandling: promhttp.ContinueOnError, 315 | }) 316 | http.HandleFunc(*metricsPath, func(w http.ResponseWriter, r *http.Request) { 317 | if !hasValidToken(r, w) { 318 | return 319 | } 320 | 321 | h.ServeHTTP(w, r) 322 | }) 323 | 324 | server := http.Server{ 325 | Addr: *listenAddress, 326 | } 327 | 328 | if *serverUseTLS { 329 | confureTLS(&server) 330 | log.Infof("Listening for %s on %s (HTTPS)", *metricsPath, *listenAddress) 331 | err = server.ListenAndServeTLS("", "") 332 | } else { 333 | log.Infof("Listening for %s on %s (HTTP)", *metricsPath, *listenAddress) 334 | err = server.ListenAndServe() 335 | } 336 | 337 | if err != nil && err != http.ErrServerClosed { 338 | log.Fatal(err) 339 | } 340 | } 341 | 342 | func hasValidToken(r *http.Request, w http.ResponseWriter) bool { 343 | if *metricsToken == "" { 344 | return true 345 | } 346 | 347 | token := r.URL.Query().Get("token") 348 | if token == "" { 349 | token = r.Header.Get("token") 350 | } 351 | 352 | if token != *metricsToken { 353 | w.WriteHeader(http.StatusForbidden) 354 | fmt.Fprint(w, "wrong token") 355 | return false 356 | } 357 | 358 | return true 359 | } 360 | 361 | func confureTLS(server *http.Server) { 362 | if *serverTlsCertFile == "" || *serverTlsKeyFile == "" { 363 | log.Error("'web.tls.cert-file' and 'web.tls.key-file' must be defined") 364 | return 365 | } 366 | 367 | server.TLSConfig = &tls.Config{ 368 | MinVersion: tls.VersionTLS12, 369 | } 370 | 371 | var err error 372 | server.TLSConfig.Certificates = make([]tls.Certificate, 1) 373 | server.TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(*serverTlsCertFile, *serverTlsKeyFile) 374 | if err != nil { 375 | log.Errorf("Loading certificates error: %v", err) 376 | return 377 | } 378 | 379 | if *serverMutualAuthEnabled { 380 | server.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert 381 | 382 | if *serverTlsCAFile != "" { 383 | var ca []byte 384 | if ca, err = os.ReadFile(*serverTlsCAFile); err != nil { 385 | log.Errorf("Loading CA error: %v", err) 386 | return 387 | } else { 388 | server.TLSConfig.ClientCAs = x509.NewCertPool() 389 | server.TLSConfig.ClientCAs.AppendCertsFromPEM(ca) 390 | } 391 | } 392 | } else { 393 | server.TLSConfig.ClientAuth = tls.NoClientCert 394 | } 395 | } 396 | 397 | func loadConfig() (*config.Config, error) { 398 | if *configFile == "" { 399 | cfg := config.Config{} 400 | addFlagToConfig(&cfg) 401 | 402 | return &cfg, nil 403 | } 404 | 405 | f, err := os.Open(*configFile) 406 | if err != nil { 407 | return nil, fmt.Errorf("cannot load config file: %w", err) 408 | } 409 | defer f.Close() 410 | 411 | cfg, err := config.FromYAML(f) 412 | if err == nil { 413 | addFlagToConfig(cfg) 414 | } 415 | 416 | return cfg, err 417 | } 418 | 419 | func setupResolver(cfg *config.Config) *net.Resolver { 420 | if cfg.DNS.Nameserver == "" { 421 | return net.DefaultResolver 422 | } 423 | 424 | if !strings.HasSuffix(cfg.DNS.Nameserver, ":53") { 425 | cfg.DNS.Nameserver += ":53" 426 | } 427 | dialer := func(ctx context.Context, _, _ string) (net.Conn, error) { 428 | d := net.Dialer{} 429 | return d.DialContext(ctx, "udp", cfg.DNS.Nameserver) 430 | } 431 | 432 | return &net.Resolver{PreferGo: true, Dial: dialer} 433 | } 434 | 435 | // addFlagToConfig updates cfg with command line flag values, unless the 436 | // config has non-zero values. 437 | func addFlagToConfig(cfg *config.Config) { 438 | if len(cfg.Targets) == 0 { 439 | cfg.Targets = make([]config.TargetConfig, len(*targetFlag)) 440 | for i, t := range *targetFlag { 441 | cfg.Targets[i] = config.TargetConfig{ 442 | Addr: t, 443 | } 444 | } 445 | } 446 | if cfg.Ping.History == 0 { 447 | cfg.Ping.History = *historySize 448 | } 449 | if cfg.Ping.Interval == 0 { 450 | cfg.Ping.Interval.Set(*pingInterval) 451 | } 452 | if cfg.Ping.Timeout == 0 { 453 | cfg.Ping.Timeout.Set(*pingTimeout) 454 | } 455 | if cfg.Ping.Size == 0 { 456 | cfg.Ping.Size = *pingSize 457 | } 458 | if cfg.DNS.Refresh == 0 { 459 | cfg.DNS.Refresh.Set(*dnsRefresh) 460 | } 461 | if cfg.DNS.Nameserver == "" { 462 | cfg.DNS.Nameserver = *dnsNameServer 463 | } 464 | if !cfg.Options.DisableIPv6 { 465 | cfg.Options.DisableIPv6 = *disableIPv6 466 | } 467 | if !cfg.Options.DisableIPv4 { 468 | cfg.Options.DisableIPv4 = *disableIPv4 469 | } 470 | if cfg.DNS.Timeout == 0 { 471 | cfg.DNS.Timeout.Set(*dnsLookupTimeout) 472 | } 473 | } 474 | 475 | const indexHTML = ` 476 | 477 | 478 | 479 | ping Exporter (Version ` + version + `) 480 | 481 | 482 |

ping Exporter

483 |

Metrics

484 |

More information:

485 |

github.com/czerwonk/ping_exporter

486 | 487 | 488 | ` 489 | -------------------------------------------------------------------------------- /rttscale.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package main 4 | 5 | import "github.com/prometheus/client_golang/prometheus" 6 | 7 | type rttUnit int 8 | 9 | const ( 10 | rttInvalid rttUnit = iota 11 | rttInMills 12 | rttInSeconds 13 | rttBoth 14 | ) 15 | 16 | func rttUnitFromString(s string) rttUnit { 17 | switch s { 18 | case "s": 19 | return rttInSeconds 20 | case "ms": 21 | return rttInMills 22 | case "both": 23 | return rttBoth 24 | default: 25 | return rttInvalid 26 | } 27 | } 28 | 29 | type scaledMetrics struct { 30 | Millis *prometheus.Desc 31 | Seconds *prometheus.Desc 32 | scale rttUnit 33 | } 34 | 35 | func (s *scaledMetrics) Describe(ch chan<- *prometheus.Desc) { 36 | if s.scale == rttInMills || s.scale == rttBoth { 37 | ch <- s.Millis 38 | } 39 | if s.scale == rttInSeconds || s.scale == rttBoth { 40 | ch <- s.Seconds 41 | } 42 | } 43 | 44 | func (s *scaledMetrics) Collect(ch chan<- prometheus.Metric, value float32, labelValues ...string) { 45 | if s.scale == rttInMills || s.scale == rttBoth { 46 | ch <- prometheus.MustNewConstMetric(s.Millis, prometheus.GaugeValue, float64(value), labelValues...) 47 | } 48 | if s.scale == rttInSeconds || s.scale == rttBoth { 49 | ch <- prometheus.MustNewConstMetric(s.Seconds, prometheus.GaugeValue, float64(value)/1000, labelValues...) 50 | } 51 | } 52 | 53 | func newScaledDesc(name, help string, scale rttUnit, variableLabels []string) scaledMetrics { 54 | return scaledMetrics{ 55 | scale: scale, 56 | Millis: newDesc(name+"_ms", help+" in millis (deprecated)", variableLabels, nil), 57 | Seconds: newDesc(name+"_seconds", help+" in seconds", variableLabels, nil), 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: ping-exporter 2 | base: core18 3 | version: git 4 | summary: A Prometheus exporter for measuring latency to servers using ICMP ping 5 | description: | 6 | ping_exporter uses the go-ping library to measure network latency to configured 7 | hosts, as an indicator of connection quality. This data is made available (exported) 8 | to prometheus, where it is stored and can be charted and queried over time. 9 | 10 | grade: stable 11 | confinement: strict 12 | 13 | apps: 14 | daemon: 15 | plugs: 16 | - network 17 | - network-bind 18 | - network-control # for raw socket support 19 | command: ping_exporter --config.path=$SNAP_COMMON/config.yaml 20 | daemon: simple 21 | 22 | parts: 23 | speedtest-exporter: 24 | plugin: go 25 | source: . 26 | -------------------------------------------------------------------------------- /tailscale.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | log "github.com/sirupsen/logrus" 8 | "tailscale.com/client/tailscale" 9 | ) 10 | 11 | func tsDiscover() { 12 | tailscale.I_Acknowledge_This_API_Is_Unstable = true 13 | 14 | client := tailscale.NewClient(*tailnet, tailscale.APIKey(os.Getenv("TS_API_KEY"))) 15 | 16 | devices, err := client.Devices(context.Background(), tailscale.DeviceAllFields) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | 21 | for _, dev := range devices { 22 | *targetFlag = append(*targetFlag, dev.Hostname) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /target.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "net" 9 | "strconv" 10 | "sync" 11 | "time" 12 | 13 | "github.com/czerwonk/ping_exporter/config" 14 | mon "github.com/digineo/go-ping/monitor" 15 | log "github.com/sirupsen/logrus" 16 | ) 17 | 18 | // ipVersion represents the IP protocol version of an address 19 | type ipVersion uint8 20 | 21 | type target struct { 22 | host string 23 | addresses []net.IPAddr 24 | delay time.Duration 25 | resolver *net.Resolver 26 | mutex sync.Mutex 27 | } 28 | 29 | type targets struct { 30 | t []*target 31 | mutex sync.RWMutex 32 | } 33 | 34 | func (t *targets) SetTargets(tar []*target) { 35 | t.mutex.Lock() 36 | defer t.mutex.Unlock() 37 | t.t = tar 38 | } 39 | 40 | func (t *targets) Contains(tar *target) bool { 41 | t.mutex.RLock() 42 | defer t.mutex.RUnlock() 43 | for _, ta := range t.t { 44 | if ta.host == tar.host { 45 | return true 46 | } 47 | } 48 | return false 49 | } 50 | 51 | func (t *targets) Get(host string) *target { 52 | t.mutex.RLock() 53 | defer t.mutex.RUnlock() 54 | for _, ta := range t.t { 55 | if ta.host == host { 56 | return ta 57 | } 58 | } 59 | return nil 60 | } 61 | 62 | func (t *targets) Targets() []*target { 63 | t.mutex.RLock() 64 | defer t.mutex.RUnlock() 65 | 66 | ret := make([]*target, len(t.t)) 67 | copy(ret, t.t) 68 | return ret 69 | } 70 | 71 | type targetOpts struct { 72 | disableIPv4 bool 73 | disableIPv6 bool 74 | } 75 | 76 | const ( 77 | ipv4 ipVersion = 4 78 | ipv6 ipVersion = 6 79 | ) 80 | 81 | func (t *target) removeFromMonitor(monitor *mon.Monitor) { 82 | for _, addr := range t.addresses { 83 | monitor.RemoveTarget(t.nameForIP(addr)) 84 | } 85 | } 86 | 87 | func (t *target) addOrUpdateMonitor(monitor *mon.Monitor, opts targetOpts, cfg *config.Config) error { 88 | t.mutex.Lock() 89 | defer t.mutex.Unlock() 90 | 91 | ctx := context.Background() 92 | if cfg.DNS.Timeout.Duration() != time.Duration(0*time.Second) { 93 | log.Infof("DNS timeout enabled: using %+v", cfg.DNS.Timeout) 94 | var cancel context.CancelFunc 95 | ctx, cancel = context.WithTimeout(context.Background(), cfg.DNS.Timeout.Duration()) 96 | defer cancel() 97 | } 98 | addrs, err := t.resolver.LookupIPAddr(ctx, t.host) 99 | if err != nil { 100 | return fmt.Errorf("error resolving target '%s': %w", t.host, err) 101 | } 102 | 103 | var sanitizedAddrs []net.IPAddr 104 | for _, addr := range addrs { 105 | if getIPVersion(addr) == ipv6 && opts.disableIPv6 { 106 | log.Infof("IPv6 disabled: skipping target for host %s (%v)", t.host, addr) 107 | continue 108 | } 109 | if getIPVersion(addr) == ipv4 && opts.disableIPv4 { 110 | log.Infof("IPv4 disabled: skipping target for host %s (%v)", t.host, addr) 111 | continue 112 | } 113 | sanitizedAddrs = append(sanitizedAddrs, addr) 114 | } 115 | 116 | for _, addr := range sanitizedAddrs { 117 | err := t.addIfNew(addr, monitor) 118 | if err != nil { 119 | return err 120 | } 121 | } 122 | 123 | t.cleanUp(sanitizedAddrs, monitor) 124 | t.addresses = sanitizedAddrs 125 | 126 | return nil 127 | } 128 | 129 | func (t *target) addIfNew(addr net.IPAddr, monitor *mon.Monitor) error { 130 | if isIPAddrInSlice(addr, t.addresses) { 131 | return nil 132 | } 133 | 134 | return t.add(addr, monitor) 135 | } 136 | 137 | func (t *target) cleanUp(addr []net.IPAddr, monitor *mon.Monitor) { 138 | for _, o := range t.addresses { 139 | if !isIPAddrInSlice(o, addr) { 140 | name := t.nameForIP(o) 141 | log.Infof("removing target for host %s (%v)", t.host, o) 142 | monitor.RemoveTarget(name) 143 | } 144 | } 145 | } 146 | 147 | func (t *target) add(addr net.IPAddr, monitor *mon.Monitor) error { 148 | name := t.nameForIP(addr) 149 | log.Infof("adding target for host %s (%v)", t.host, addr) 150 | 151 | return monitor.AddTargetDelayed(name, addr, t.delay) 152 | } 153 | 154 | func (t *target) nameForIP(addr net.IPAddr) string { 155 | return fmt.Sprintf("%s %s %s", t.host, addr.IP, getIPVersion(addr)) 156 | } 157 | 158 | func isIPAddrInSlice(ipa net.IPAddr, slice []net.IPAddr) bool { 159 | for _, x := range slice { 160 | if x.IP.Equal(ipa.IP) { 161 | return true 162 | } 163 | } 164 | 165 | return false 166 | } 167 | 168 | // getIPVersion returns the version of IP protocol used for a given address 169 | func getIPVersion(addr net.IPAddr) ipVersion { 170 | if addr.IP.To4() == nil { 171 | return ipv6 172 | } 173 | 174 | return ipv4 175 | } 176 | 177 | // String converts ipVersion to a string represention of the IP version used (i.e. "4" or "6") 178 | func (ipv ipVersion) String() string { 179 | return strconv.Itoa(int(ipv)) 180 | } 181 | -------------------------------------------------------------------------------- /target_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "net" 8 | "os" 9 | "sync" 10 | "testing" 11 | 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | var ( 16 | ipv4Addr, ipv6Addr, ipv4AddrGoogle, ipv6AddrGoogle []net.IPAddr 17 | ) 18 | 19 | func TestMain(m *testing.M) { 20 | // --- set up test addresses ---- // 21 | var err error 22 | ipv4Addr, err = net.DefaultResolver.LookupIPAddr(context.TODO(), "127.0.0.1") 23 | if err != nil || len(ipv4Addr) < 1 { 24 | log.Fatal("skipping test, cannot resolve 127.0.0.1 to net.IPAddr") 25 | return 26 | } 27 | 28 | ipv6Addr, err = net.DefaultResolver.LookupIPAddr(context.TODO(), "::1") 29 | if err != nil || len(ipv6Addr) < 1 { 30 | log.Fatal("skipping test, cannot resolve ::1 to net.IPAddr") 31 | return 32 | } 33 | 34 | ipv4AddrGoogle, err = net.DefaultResolver.LookupIPAddr(context.TODO(), "142.250.72.206") 35 | if err != nil || len(ipv4Addr) < 1 { 36 | log.Fatal("skipping test, cannot resolve 142.250.72.206 to net.IPAddr") 37 | return 38 | } 39 | 40 | ipv6AddrGoogle, err = net.DefaultResolver.LookupIPAddr(context.TODO(), "2607:f8b0:4005:810::200e") 41 | if err != nil || len(ipv6Addr) < 1 { 42 | log.Fatal("skipping test, cannot resolve 2607:f8b0:4005:810::200e to net.IPAddr") 43 | return 44 | } 45 | 46 | os.Exit(m.Run()) 47 | } 48 | 49 | func Test_ipVersion_String(t *testing.T) { 50 | tests := []struct { 51 | name string 52 | ipv ipVersion 53 | want string 54 | }{ 55 | { 56 | "ipv6", 57 | ipv6, 58 | "6", 59 | }, 60 | { 61 | "ipv4", 62 | ipv4, 63 | "4", 64 | }, 65 | } 66 | for _, tt := range tests { 67 | t.Run(tt.name, func(t *testing.T) { 68 | if got := tt.ipv.String(); got != tt.want { 69 | t.Errorf("IPVersion.String() = %v, want %v", got, tt.want) 70 | } 71 | }) 72 | } 73 | } 74 | 75 | func Test_getIPVersion(t *testing.T) { 76 | tests := []struct { 77 | name string 78 | addr net.IPAddr 79 | want ipVersion 80 | }{ 81 | { 82 | "ipv4", 83 | ipv4Addr[0], 84 | ipv4, 85 | }, 86 | { 87 | "ipv6", 88 | ipv6Addr[0], 89 | ipv6, 90 | }, 91 | { 92 | "ipv4-google", 93 | ipv4AddrGoogle[0], 94 | ipv4, 95 | }, 96 | { 97 | "ipv6-google", 98 | ipv6AddrGoogle[0], 99 | ipv6, 100 | }, 101 | } 102 | for _, tt := range tests { 103 | t.Run(tt.name, func(t *testing.T) { 104 | if got := getIPVersion(tt.addr); got != tt.want { 105 | t.Errorf("getIPVersion() = %v, want %v", got, tt.want) 106 | } 107 | }) 108 | } 109 | } 110 | 111 | func Test_target_nameForIP(t *testing.T) { 112 | tests := []struct { 113 | name string 114 | addr net.IPAddr 115 | want string 116 | }{ 117 | { 118 | "ipv4-localhost", 119 | ipv4Addr[0], 120 | "testhost.com 127.0.0.1 4", 121 | }, 122 | { 123 | "ipv6-localhost", 124 | ipv6Addr[0], 125 | "testhost.com ::1 6", 126 | }, 127 | { 128 | "ipv4-google", 129 | ipv4AddrGoogle[0], 130 | "testhost.com 142.250.72.206 4", 131 | }, 132 | { 133 | "ipv6-google", 134 | ipv6AddrGoogle[0], 135 | "testhost.com 2607:f8b0:4005:810::200e 6", 136 | }, 137 | } 138 | for _, tt := range tests { 139 | tr := &target{ 140 | host: "testhost.com", 141 | addresses: []net.IPAddr{}, 142 | delay: 0, 143 | resolver: &net.Resolver{}, 144 | mutex: sync.Mutex{}, 145 | } 146 | t.Run(tt.name, func(t *testing.T) { 147 | if got := tr.nameForIP(tt.addr); got != tt.want { 148 | t.Errorf("target.nameForIP() = %v, want %v", got, tt.want) 149 | } 150 | }) 151 | } 152 | } 153 | --------------------------------------------------------------------------------