Compare commits

..

No commits in common. "master" and "0.0.3" have entirely different histories.

23 changed files with 241 additions and 4635 deletions

View file

@ -1,71 +0,0 @@
# Contributing
Pi-hole exporter is an open source project, completely opened to be a community-driven project.
If you'd like to contribute, you are free to do so.
## Why contributing?
The best way to start contributing is to help others. This includes:
- Reporting new bugs or adding details to existing ones
- Trying to reproduce unconfirmed bugs
- Quick typo fix or documentation improvement
- Participating in discussions
After becoming familiar with this project, you could contribute in other ways, such as:
- Helping fix bugs (issues)
- Implement new features you think it could be interesting for others
- Publishing guides, tutorials, and examples
## Questions
If you have a question, please feel free to open a new issue on this repository.
## Bug Reports
Search through [Github Issues](https://github.com/eko/pihole-exporter/issues) to see if the bug has already been reported. If so, please comment with any additional information.
New bug reports must include:
1. Detailed description of faulty behavior
2. Steps for reproduction or failing test case
3. Expected and actual behaviors
4. Platforms (OS **and** versions) affected
5. Any information you think it could help identifing the cause of your issue
Lacking reports may be autoclosed with a link to these instructions.
## Feature Requests
Search through [Github Pull Requests](https://github.com/eko/pihole-exporter/pulls) to see if someone has already suggested the feature. If so, please provide support with a [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments) and add your own use case.
To open a new feature request, please include:
1. A detailed description of the feature
2. Why this feature belongs in project core, instead of your own application logic
3. Background of where and how you are using the project
4. The use case that would be enabled or improved for your product, if the feature was implemented
Features are prioritized based on real world users and use cases, not theoretically useful additions for other unknown users. Lacking feature requests may be autoclosed with a link to this section.
The more complete and compelling the request, the more likely it will ultimately be implemented. Garnering community support will help as well!
## Pull Requests
Non-code Pull Requests such as typo fixes or documentation improvements are highly encouraged and are often accepted immediately.
Pull Requests modifying source code, including backwards compatible additions, will undergo the most scrutiny, and will almost certainly require a proper discussion of the motivation and merits beforehand. Simply increasing code complexity is a cost not to be taken lightly.
Pull requests must:
1. Be forked off the [master](https://github.com/eko/pihole-exporter/tree/master) branch.
2. Pass the linter and conform to existing coding styles.
3. Commits are [squashed](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Squashing-Commits) to minimally coherent units of changes.
4. Are accompanied by tests covering the new feature or demonstrating the bug for fixes.
5. Serve a single atomic purpose (add one feature or fix one bug).
6. Introduce only changes that further the PR's singular purpose (ex. do not tweak an unrelated config along with adding your feature).
7. Not break any existing unit or end to end tests.
**Important:** By issuing a Pull Request you agree to allow the project owners to license your work under the terms of the [License](https://github.com/eko/pihole-exporter/blob/master/LICENSE).

View file

@ -1,29 +0,0 @@
---
name: Issue template
about: Issue template
title: ''
labels: ''
assignees: ''
---
Please describe the a concise description and fill out the details below. It will help others efficiently understand your request and get to an answer instead of repeated back and forth. Providing a [minimal, complete and verifiable example](https://stackoverflow.com/help/mcve) will further increase your chances that someone can help.
**Steps for Reproduction**
1. Set up the following "xxxx" value in the `config.yaml` configuration file
2. Launch binary with the following arguments
3. Step Two
4. Step Three
**Expected behavior**:
**Actual behavior**:
**Platforms**:
Include browser, operating system and respective versions
**Versions**:
Which versions are you running?

View file

@ -1,14 +0,0 @@
version: 2
updates:
- package-ecosystem: gomod
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
- package-ecosystem: docker
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10

View file

@ -1,52 +0,0 @@
name: Test (master)
on:
push:
branches:
- master
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go_version: [ '1.20' ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go_version }}
- name: Install go dependencies
run: go get -t -v ./...
- name: Run go tests
run: go test -v -cover -race ./...
docker-build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v3
- uses: azure/docker-login@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up docker buildx
id: buildx
uses: crazy-max/ghaction-docker-buildx@v1
with:
buildx-version: latest
qemu-version: latest
- name: Docker buildx available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run docker buildx build
run: |
docker buildx build \
--platform linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 \
--output=type=registry,push=true \
--tag ekofr/pihole-exporter:latest \
.

View file

@ -1,138 +0,0 @@
name: Build/Push (tag)
on:
push:
tags:
- '*'
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go_version: [ '1.20' ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go_version }}
- run: go get -t -v ./...
- run: go test -v -race ./...
docker-build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v3
- uses: azure/docker-login@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up docker buildx
id: buildx
uses: crazy-max/ghaction-docker-buildx@v1
with:
buildx-version: latest
qemu-version: latest
- name: Docker buildx available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Get the version
id: get_version
run: echo ::set-output name=TAG_NAME::${GITHUB_REF/refs\/tags\//}
- name: Run docker buildx build
run: |
docker buildx build \
--platform linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 \
--output=type=registry,push=true \
--tag ekofr/pihole-exporter:${{ steps.get_version.outputs.TAG_NAME }} \
.
release:
runs-on: ubuntu-latest
needs: test
steps:
- name: Create release
id: create
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
prerelease: false
outputs:
upload_url: ${{ steps.create.outputs.upload_url }}
upload:
needs: release
strategy:
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: 386
- os: ubuntu-latest
goos: linux
goarch: amd64
- os: ubuntu-latest
goos: linux
goarch: arm
- os: macos-latest
goos: darwin
goarch: amd64
- os: macos-latest
goos: darwin
goarch: arm64
- os: ubuntu-latest
goos: windows
goarch: 386
extension: '.exe'
- os: ubuntu-latest
goos: windows
goarch: amd64
extension: '.exe'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '1.20'
- name: Download go dependencies
run: go get -t -v ./...
# Release File
- name: Create release
run: GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -ldflags "-s -w" -o pihole_exporter-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.extension }} ./
- name: Upload release assets
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}${{ matrix.extension }}
asset_path: ./pihole_exporter-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.extension }}
asset_name: pihole_exporter-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.extension }}
asset_content_type: application/octet-stream
# SHA256 Sum File
- name: Create SHA256 sum
run: sha256sum pihole_exporter-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.extension }} > pihole_exporter-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.extension }}.sha256.txt
- name: Upload release assets
id: upload-release-sum-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}${{ matrix.extension }}.sha256.txt
asset_path: ./pihole_exporter-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.extension }}.sha256.txt
asset_name: pihole_exporter-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.extension }}.sha256.txt
asset_content_type: application/octet-stream

3
.gitignore vendored
View file

@ -1,7 +1,6 @@
vendor
search-api
bin
tmp
.vscode
report.xml
debug

View file

@ -1,18 +0,0 @@
repos:
- repo: https://github.com/dnephin/pre-commit-golang
rev: v0.5.1
hooks:
- id: go-fmt
- id: go-vet
- id: validate-toml
- id: no-go-testing
- id: go-unit-tests
- id: go-build
- id: go-mod-tidy
- repo: https://github.com/pre-commit/pre-commit-hooks.git
rev: v4.3.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- id: mixed-line-ending
- id: check-merge-conflict

42
.travis.yml Normal file
View file

@ -0,0 +1,42 @@
language: go
matrix:
include:
- go: 1.x
env: LATEST=true
- go: 1.8.x
- go: 1.9.x
- go: 1.10.x
- go: 1.11.x
- go: 1.12.x
- go: tip
allow_failures:
- go: tip
before_install:
- go get github.com/mitchellh/gox
install:
- export GO111MODULE=on
- go get -t -v ./...
script:
- go test -v -race ./...
- if [ "${LATEST}" = "true" ]; then gox -os="linux darwin windows" -arch="386 amd64" -osarch="linux/arm" -output="pihole_exporter-{{.OS}}-{{.Arch}}" -verbose ./...; fi
deploy:
provider: releases
skip_cleanup: true
api_key:
secure: $GITHUB_TOKEN
file:
- pihole_exporter-windows-386.exe
- pihole_exporter-windows-amd64.exe
- pihole_exporter-darwin-386
- pihole_exporter-darwin-amd64
- pihole_exporter-linux-386
- pihole_exporter-linux-amd64
- pihole_exporter-linux-arm
on:
repo: eko/pihole-exporter
tags: true
condition: $LATEST = true

View file

@ -1,22 +0,0 @@
ARG IMAGE=scratch
ARG OS=linux
ARG ARCH=amd64
FROM golang:1.21.2-alpine3.17 as builder
WORKDIR /go/src/github.com/eko/pihole-exporter
COPY . .
RUN apk --no-cache add git alpine-sdk
RUN GO111MODULE=on go mod vendor
RUN CGO_ENABLED=0 GOOS=$OS GOARCH=$ARCH go build -ldflags '-s -w' -o binary ./
FROM $IMAGE
LABEL name="pihole-exporter"
WORKDIR /root/
COPY --from=builder /go/src/github.com/eko/pihole-exporter/binary pihole-exporter
CMD ["./pihole-exporter"]

144
README.md
View file

@ -1,17 +1,14 @@
# Pi-hole Prometheus Exporter
# PI-Hole Prometheus Exporter
![Build/Push (master)](https://github.com/eko/pihole-exporter/workflows/Build/Push%20(master)/badge.svg)
[![TravisBuildStatus](https://api.travis-ci.org/eko/pihole-exporter.svg?branch=master)](https://travis-ci.org/eko/pihole-exporte)
[![GoDoc](https://godoc.org/github.com/eko/pihole-exporter?status.png)](https://godoc.org/github.com/eko/pihole-exporter)
[![GoReportCard](https://goreportcard.com/badge/github.com/eko/pihole-exporter)](https://goreportcard.com/report/github.com/eko/pihole-exporter)
This is a Prometheus exporter for [Pi-hole](https://pi-hole.net/)'s Raspberry PI ad blocker.
This is a Prometheus exporter for [PI-Hole](https://pi-hole.net/)'s Raspberry PI ad blocker.
![Grafana dashboard](https://raw.githubusercontent.com/eko/pihole-exporter/master/dashboard.jpg)
Available Grafana Dasboards:
* Prometheus: [Grafana Labs](https://grafana.com/grafana/dashboards/10176-pi-hole-exporter/) / [JSON/Github](https://raw.githubusercontent.com/eko/pihole-exporter/master/grafana/dashboard.json) --> [Preview](https://raw.githubusercontent.com/eko/pihole-exporter/master/dashboard.jpg)
* InfluxDB 2 (Flux): [Grafana Labs](https://grafana.com/grafana/dashboards/17094-pi-hole-exporter-influxdb-2/) / [JSON/Github](https://raw.githubusercontent.com/eko/pihole-exporter/master/grafana/dashboard-influxdb2.json) --> [Preview](https://raw.githubusercontent.com/eko/pihole-exporter/master/dashboard-influxdb2.png)
Grafana dashboard is [available here](https://grafana.com/dashboards/10176).
## Prerequisites
@ -24,6 +21,7 @@ Available Grafana Dasboards:
You can download the latest version of the binary built for your architecture here:
* Architecture **i386** [
[Darwin](https://github.com/eko/pihole-exporter/releases/latest/download/pihole_exporter-darwin-386) /
[Linux](https://github.com/eko/pihole-exporter/releases/latest/download/pihole_exporter-linux-386) /
[Windows](https://github.com/eko/pihole-exporter/releases/latest/download/pihole_exporter-windows-386.exe)
]
@ -33,92 +31,18 @@ You can download the latest version of the binary built for your architecture he
[Windows](https://github.com/eko/pihole-exporter/releases/latest/download/pihole_exporter-windows-amd64.exe)
]
* Architecture **arm** [
[Darwin](https://github.com/eko/pihole-exporter/releases/latest/download/pihole_exporter-darwin-arm64) /
[Linux](https://github.com/eko/pihole-exporter/releases/latest/download/pihole_exporter-linux-arm)
]
### Using Docker
The exporter is also available as a [Docker image](https://hub.docker.com/r/ekofr/pihole-exporter).
You can run it using the following example and pass configuration environment variables:
```
$ docker run \
-e 'PIHOLE_HOSTNAME=192.168.1.2' \
-e 'PIHOLE_PASSWORD=mypassword' \
-e 'PORT=9617' \
-p 9617:9617 \
ekofr/pihole-exporter:latest
```
Or use PiHole's `WEBPASSWORD` as an API token instead of the password
```bash
$ API_TOKEN=$(awk -F= -v key="WEBPASSWORD" '$1==key {print $2}' /etc/pihole/setupVars.conf)
$ docker run \
-e 'PIHOLE_HOSTNAME=192.168.1.2' \
-e "PIHOLE_API_TOKEN=$API_TOKEN" \
-e 'PORT=9617' \
-p 9617:9617 \
ekofr/pihole-exporter:latest
```
If you are running pi-hole behind https, you must both set the `PIHOLE_PROTOCOL` environment variable
as well as include your ssl certificates to the docker image as it does not have any baked in:
```
$ docker run \
-e 'PIHOLE_PROTOCOL=https' \
-e 'PIHOLE_HOSTNAME=192.168.1.2' \
-e 'PIHOLE_PASSWORD=mypassword' \
-e 'PORT=9617' \
-v '/etc/ssl/certs:/etc/ssl/certs:ro' \
-p 9617:9617 \
ekofr/pihole-exporter:latest
```
A single instance of pihole-exporter can monitor multiple pi-holes instances.
To do so, you can specify a list of hostnames, protocols, passwords/API tokens and ports by separating them with commas in their respective environment variable:
```
$ docker run \
-e 'PIHOLE_PROTOCOL=http,http,http" \
-e 'PIHOLE_HOSTNAME=192.168.1.2,192.168.1.3,192.168.1.4"' \
-e "PIHOLE_API_TOKEN=$API_TOKEN1,$API_TOKEN2,$API_TOKEN3" \
-e "PIHOLE_PORT=8080,8081,8080" \
-e 'PORT=9617' \
-p 9617:9617 \
ekofr/pihole-exporter:latest
```
If port, protocol and API token/password is the same for all instances, you can specify them only once:
```
$ docker run \
-e 'PIHOLE_PROTOCOL=http" \
-e 'PIHOLE_HOSTNAME=192.168.1.2,192.168.1.3,192.168.1.4"' \
-e "PIHOLE_API_TOKEN=$API_TOKEN" \
-e "PIHOLE_PORT=8080" \
-e 'PORT=9617' \
-p 9617:9617 \
ekofr/pihole-exporter:latest
```
### From sources
Optionally, you can download and build it from the sources. You have to retrieve the project sources by using one of the following way:
```bash
$ go install github.com/eko/pihole-exporter@latest
$ go get -u github.com/eko/pihole-exporter
# or
$ git clone https://github.com/eko/pihole-exporter.git
```
Install the needed vendors:
```
$ GO111MODULE=on go mod vendor
```
Then, build the binary (here, an example to run on Raspberry PI ARM architecture):
```bash
$ GOOS=linux GOARCH=arm GOARM=7 go build -o pihole_exporter .
@ -128,27 +52,16 @@ $ GOOS=linux GOARCH=arm GOARM=7 go build -o pihole_exporter .
In order to run the exporter, type the following command (arguments are optional):
Using a password
```bash
$ ./pihole_exporter -pihole_hostname 192.168.1.10 -pihole_password azerty
```
Or use PiHole's `WEBPASSWORD` as an API token instead of the password
```bash
$ API_TOKEN=$(awk -F= -v key="WEBPASSWORD" '$1==key {print $2}' /etc/pihole/setupVars.conf)
$ ./pihole_exporter -pihole_hostname 192.168.1.10 -pihole_api_token $API_TOKEN
```
```bash
2019/05/09 20:19:52 ------------------------------------
2019/05/09 20:19:52 - Pi-hole exporter configuration -
2019/05/09 20:19:52 - PI-Hole exporter configuration -
2019/05/09 20:19:52 ------------------------------------
2019/05/09 20:19:52 PIHoleHostname : 192.168.1.10
2019/05/09 20:19:52 PIHolePassword : azerty
2019/05/09 20:19:52 Port : 9617
2019/05/09 20:19:52 Timeout : 5s
2019/05/09 20:19:52 Port : 9311
2019/05/09 20:19:52 Interval : 10s
2019/05/09 20:19:52 ------------------------------------
2019/05/09 20:19:52 New Prometheus metric registered: domains_blocked
2019/05/09 20:19:52 New Prometheus metric registered: dns_queries_today
@ -178,28 +91,22 @@ Once the exporter is running, you also have to update your `prometheus.yml` conf
scrape_configs:
- job_name: 'pihole'
static_configs:
- targets: ['localhost:9617']
- targets: ['localhost:9311']
```
## Available CLI options
```bash
# Hostname of the host(s) where Pi-hole is installed
# Interval of time the exporter will fetch data from PI-Hole
-interval duration (optional) (default 10s)
# Hostname of the Raspberry PI where PI-Hole is installed
-pihole_hostname string (optional) (default "127.0.0.1")
# Password defined on the Pi-hole interface
# Password defined on the PI-Hole interface
-pihole_password string (optional)
# Timeout to connect and retrieve data from a Pi-hole instance
-timeout duration (optional) (default 5s)
# WEBPASSWORD / api token defined on the Pi-hole interface at `/etc/pihole/setupVars.conf`
-pihole_api_token string (optional)
# Address to be used for the exporter
-bind_addr string (optional) (default "0.0.0.0")
# Port to be used for the exporter
-port string (optional) (default "9617")
-port string (optional) (default "9311")
```
## Available Prometheus metrics
@ -217,16 +124,9 @@ scrape_configs:
| pihole_unique_clients | This represent the number of unique clients seen |
| pihole_dns_queries_all_types | This represent the number of DNS queries made for all types |
| pihole_reply | This represent the number of replies made for all types |
| pihole_top_queries | This represent the number of top queries made by Pi-hole by domain |
| pihole_top_ads | This represent the number of top ads made by Pi-hole by domain |
| pihole_top_sources | This represent the number of top sources requests made by Pi-hole by source host |
| pihole_forward_destinations | This represent the number of forward destinations requests made by Pi-hole by destination |
| pihole_querytypes | This represent the number of queries made by Pi-hole by type |
| pihole_status | This represent if Pi-hole is enabled |
## Pihole-Exporter Helm Chart
[Link](https://github.com/SiM22/pihole-exporter-helm-chart)
This is a simple Helm Chart to deploy the exporter in a kubernetes cluster.
| pihole_top_queries | This represent the number of top queries made by PI-Hole by domain |
| pihole_top_ads | This represent the number of top ads made by PI-Hole by domain |
| pihole_top_sources | This represent the number of top sources requests made by PI-Hole by source host |
| pihole_forward_destinations | This represent the number of forward destinations requests made by PI-Hole by destination |
| pihole_querytypes | This represent the number of queries made by PI-Hole by type |
| pihole_status | This represent if PI-Hole is enabled |

View file

@ -2,15 +2,11 @@ package config
import (
"context"
"errors"
"fmt"
"log"
"reflect"
"runtime"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/heetch/confita"
"github.com/heetch/confita/backend"
"github.com/heetch/confita/backend/env"
@ -19,41 +15,25 @@ import (
// Config is the exporter CLI configuration.
type Config struct {
PIHoleProtocol string `config:"pihole_protocol"`
PIHoleHostname string `config:"pihole_hostname"`
PIHolePort uint16 `config:"pihole_port"`
PIHolePassword string `config:"pihole_password"`
PIHoleApiToken string `config:"pihole_api_token"`
BindAddr string `config:"bind_addr"`
Port uint16 `config:"port"`
Port string `config:"port"`
Interval time.Duration `config:"interval"`
}
type EnvConfig struct {
PIHoleProtocol []string `config:"pihole_protocol"`
PIHoleHostname []string `config:"pihole_hostname"`
PIHolePort []uint16 `config:"pihole_port"`
PIHolePassword []string `config:"pihole_password"`
PIHoleApiToken []string `config:"pihole_api_token"`
BindAddr string `config:"bind_addr"`
Port uint16 `config:"port"`
Timeout time.Duration `config:"timeout"`
}
func getDefaultConfig() *Config {
return &Config{
PIHoleHostname: "127.0.0.1",
PIHolePassword: "",
func getDefaultEnvConfig() *EnvConfig {
return &EnvConfig{
PIHoleProtocol: []string{"http"},
PIHoleHostname: []string{"127.0.0.1"},
PIHolePort: []uint16{80},
PIHolePassword: []string{},
PIHoleApiToken: []string{},
BindAddr: "0.0.0.0",
Port: 9617,
Timeout: 5 * time.Second,
Port: "9311",
Interval: 10 * time.Second,
}
}
// Load method loads the configuration by using both flag or environment variables.
func Load() (*EnvConfig, []Config, error) {
func Load() *Config {
loaders := []backend.Backend{
env.NewBackend(),
flags.NewBackend(),
@ -61,7 +41,7 @@ func Load() (*EnvConfig, []Config, error) {
loader := confita.NewLoader(loaders...)
cfg := getDefaultEnvConfig()
cfg := getDefaultConfig()
err := loader.Load(context.Background(), cfg)
if err != nil {
panic(err)
@ -69,150 +49,19 @@ func Load() (*EnvConfig, []Config, error) {
cfg.show()
if clientsConfig, err := cfg.Split(); err != nil {
return cfg, nil, err
} else {
return cfg, clientsConfig, nil
}
return cfg
}
func (c *Config) String() string {
ref := reflect.ValueOf(c)
fields := ref.Elem()
buffer := make([]string, fields.NumField(), fields.NumField())
for i := 0; i < fields.NumField(); i++ {
valueField := fields.Field(i)
typeField := fields.Type().Field(i)
if typeField.Name != "PIHolePassword" && typeField.Name != "PIHoleApiToken" {
buffer[i] = fmt.Sprintf("%s=%v", typeField.Name, valueField.Interface())
} else if valueField.Len() > 0 {
buffer[i] = fmt.Sprintf("%s=%s", typeField.Name, "*****")
}
}
buffer = removeEmptyString(buffer)
return fmt.Sprintf("<Config@%X %s>", &c, strings.Join(buffer, ", "))
}
// Validate check if the config is valid
func (c Config) Validate() error {
if c.PIHoleProtocol != "http" && c.PIHoleProtocol != "https" {
return fmt.Errorf("protocol %s is invalid. Must be http or https", c.PIHoleProtocol)
}
return nil
}
func (c EnvConfig) Split() ([]Config, error) {
hostsCount := len(c.PIHoleHostname)
result := make([]Config, 0, hostsCount)
for i, hostname := range c.PIHoleHostname {
config := Config{
PIHoleHostname: strings.TrimSpace(hostname),
}
if len(c.PIHolePort) == 1 {
config.PIHolePort = c.PIHolePort[0]
} else if len(c.PIHolePort) == hostsCount {
config.PIHolePort = c.PIHolePort[i]
} else if len(c.PIHolePort) != 0 {
return nil, errors.New("Wrong number of ports. Port can be empty to use default, one value to use for all hosts, or match the number of hosts")
}
if hasData, data, isValid := extractStringConfig(c.PIHoleProtocol, i, hostsCount); hasData {
config.PIHoleProtocol = data
} else if !isValid {
return nil, errors.New("Wrong number of PIHoleProtocol. PIHoleProtocol can be empty to use default, one value to use for all hosts, or match the number of hosts")
}
if hasData, data, isValid := extractStringConfig(c.PIHoleApiToken, i, hostsCount); hasData {
config.PIHoleApiToken = data
} else if !isValid {
return nil, errors.New(fmt.Sprintf("Wrong number of PIHoleApiToken %d (Hosts: %d). PIHoleApiToken can be empty to use default, one value to use for all hosts, or match the number of hosts", len(c.PIHoleApiToken), hostsCount))
}
if hasData, data, isValid := extractStringConfig(c.PIHolePassword, i, hostsCount); hasData {
config.PIHolePassword = data
} else if !isValid {
return nil, errors.New("Wrong number of PIHolePassword. PIHolePassword can be empty to use default, one value to use for all hosts, or match the number of hosts")
}
result = append(result, config)
}
return result, nil
}
func extractStringConfig(data []string, idx int, hostsCount int) (bool, string, bool) {
if len(data) == 1 {
v := strings.TrimSpace(data[0])
if v != "" {
return true, v, true
}
} else if len(data) == hostsCount {
v := strings.TrimSpace(data[idx])
if v != "" {
return true, v, true
}
} else if len(data) != 0 { //Host count missmatch
return false, "", false
}
// Empty
return false, "", true
}
func removeEmptyString(source []string) []string {
var result []string
for _, s := range source {
if s != "" {
result = append(result, s)
}
}
return result
}
func (c Config) hostnameURL() string {
s := fmt.Sprintf("%s://%s", c.PIHoleProtocol, c.PIHoleHostname)
if c.PIHolePort != 0 {
s += fmt.Sprintf(":%d", c.PIHolePort)
}
return s
}
// PIHoleStatsURL returns the stats url
func (c Config) PIHoleStatsURL() string {
return c.hostnameURL() + "/admin/api.php?summaryRaw&overTimeData&topItems&recentItems&getQueryTypes&getForwardDestinations&getQuerySources&jsonForceObject"
}
// PIHoleLoginURL returns the login url
func (c Config) PIHoleLoginURL() string {
return c.hostnameURL() + "/admin/index.php?login"
}
func (c EnvConfig) show() {
func (c Config) show() {
val := reflect.ValueOf(&c).Elem()
log.Info("------------------------------------")
log.Info("- Pi-hole exporter configuration -")
log.Info("------------------------------------")
log.Info("Go version: ", runtime.Version())
log.Println("------------------------------------")
log.Println("- PI-Hole exporter configuration -")
log.Println("------------------------------------")
for i := 0; i < val.NumField(); i++ {
valueField := val.Field(i)
typeField := val.Type().Field(i)
// Do not print password or api token but do print the authentication method
if typeField.Name != "PIHolePassword" && typeField.Name != "PIHoleApiToken" {
log.Info(fmt.Sprintf("%s : %v", typeField.Name, valueField.Interface()))
} else {
showAuthenticationMethod(typeField.Name, valueField.Len())
}
}
log.Info("------------------------------------")
}
func showAuthenticationMethod(name string, length int) {
if length > 0 {
log.Info(fmt.Sprintf("Pi-hole Authentication Method : %s", name))
log.Println(fmt.Sprintf("%s : %v", typeField.Name, valueField.Interface()))
}
log.Println("------------------------------------")
}

View file

@ -1,154 +0,0 @@
package config
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
const (
PIHOLE_HOSTNAME = "PIHOLE_HOSTNAME"
PIHOLE_PORT = "PIHOLE_PORT"
PIHOLE_API_TOKEN = "PIHOLE_API_TOKEN"
PIHOLE_PROTOCOL = "PIHOLE_PROTOCOL"
)
type EnvInitiazlier func(*testing.T)
type TestCase struct {
Name string
Initializer EnvInitiazlier
}
func TestSplitDefault(t *testing.T) {
assert := assert.New(t)
env := getDefaultEnvConfig()
clientConfigs, err := env.Split()
assert.NoError(err)
clientConfig := clientConfigs[0]
assert.Equal("127.0.0.1", clientConfig.PIHoleHostname)
assert.Equal("http", clientConfig.PIHoleProtocol)
assert.Equal(uint16(80), clientConfig.PIHolePort)
assert.Empty(clientConfig.PIHoleApiToken)
assert.Empty(clientConfig.PIHolePassword)
}
func TestSplitMultipleHostWithSameConfig(t *testing.T) {
assert := assert.New(t)
env := getDefaultEnvConfig()
env.PIHoleHostname = []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}
env.PIHoleApiToken = []string{"api-token"}
env.PIHolePort = []uint16{8080}
clientConfigs, err := env.Split()
assert.NoError(err)
assert.Len(clientConfigs, 3)
testCases := []struct {
Host string
Port uint16
Protocol string
}{
{
Host: "127.0.0.1",
Port: 8080,
Protocol: "http",
},
{
Host: "127.0.0.2",
Port: 8080,
Protocol: "http",
},
{
Host: "127.0.0.3",
Port: 8080,
Protocol: "http",
},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("Test %s", tc.Host), func(t *testing.T) {
clientConfig := clientConfigs[i]
assert.Equal(tc.Host, clientConfig.PIHoleHostname)
assert.Equal(tc.Protocol, clientConfig.PIHoleProtocol)
assert.Equal(tc.Port, clientConfig.PIHolePort)
assert.Equal("api-token", clientConfig.PIHoleApiToken)
assert.Empty(clientConfig.PIHolePassword)
})
}
}
func TestSplitMultipleHostWithMultipleConfigs(t *testing.T) {
assert := assert.New(t)
env := getDefaultEnvConfig()
env.PIHoleHostname = []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}
env.PIHoleApiToken = []string{"api-token1", "", "api-token3"}
env.PIHolePassword = []string{"", "password2", ""}
env.PIHolePort = []uint16{8081, 8082, 8083}
clientConfigs, err := env.Split()
assert.NoError(err)
assert.Len(clientConfigs, 3)
testCases := []struct {
Host string
Port uint16
Protocol string
ApiToken string
Password string
}{
{
Host: "127.0.0.1",
Port: 8081,
Protocol: "http",
ApiToken: "api-token1",
Password: "",
},
{
Host: "127.0.0.2",
Port: 8082,
Protocol: "http",
ApiToken: "",
Password: "password2",
},
{
Host: "127.0.0.3",
Port: 8083,
Protocol: "http",
ApiToken: "api-token3",
Password: "",
},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("Test %s", tc.Host), func(t *testing.T) {
clientConfig := clientConfigs[i]
assert.Equal(tc.Host, clientConfig.PIHoleHostname)
assert.Equal(tc.Protocol, clientConfig.PIHoleProtocol)
assert.Equal(tc.Port, clientConfig.PIHolePort)
assert.Equal(tc.ApiToken, clientConfig.PIHoleApiToken)
assert.Equal(tc.Password, clientConfig.PIHolePassword)
})
}
}
func TestWrongParams(t *testing.T) {
assert := assert.New(t)
env := getDefaultEnvConfig()
env.PIHoleHostname = []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}
env.PIHoleApiToken = []string{"api-token1", "api-token2"}
env.PIHolePort = []uint16{808}
clientConfigs, err := env.Split()
assert.Errorf(err, "Wrong number of PIHoleApiToken. PIHoleApiToken can be empty to use default, one value to use for all hosts, or match the number of hosts")
assert.Nil(clientConfigs)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

View file

@ -1,19 +0,0 @@
version: "3.6"
services:
pihole_exporter:
build:
context: ./
args:
ARCH: CHANGE_ME
dockerfile: Dockerfile
image: ekofr/pihole-exporter:arm64
container_name: pihole_exporter
expose:
- 9617
environment:
PIHOLE_HOSTNAME: CHANGE_ME
PIHOLE_PORT: CHANGE_ME
PIHOLE_PASSWORD: CHANGE_ME
INTERVAL: 30s
PORT: 9617
restart: always

29
go.mod
View file

@ -1,28 +1,11 @@
module github.com/eko/pihole-exporter
go 1.20
go 1.12
require (
github.com/heetch/confita v0.10.0
github.com/prometheus/client_golang v1.17.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.4
github.com/xonvanetta/shutdown v0.0.3
golang.org/x/net v0.16.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
golang.org/x/sys v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
github.com/heetch/confita v0.5.1
github.com/pkg/errors v0.8.1 // indirect
github.com/prometheus/client_golang v0.9.2
github.com/stretchr/testify v1.3.0 // indirect
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c
)

250
go.sum
View file

@ -1,253 +1,33 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.3+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.8.6/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/heetch/confita v0.10.0 h1:00V4eQPDU71v9nZD7N/DsSb9cnPJh59CjrpQPfln47A=
github.com/heetch/confita v0.10.0/go.mod h1:W6GDCVPvi2LpvdEriwZTu2fyxuK+Grx1vY302gtWfvM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/heetch/confita v0.5.1 h1:EiE32j+Ze0sI0YBeJDSdqTZ32uKz2XCTQIzSgwgfnvk=
github.com/heetch/confita v0.5.1/go.mod h1:S8Em4kuK8pR5vfTiaNkFLfNDMlGF/EtQUaCxDhXRpCs=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xonvanetta/shutdown v0.0.3 h1:Gf9Rh0kEJgUjV8ZmG08t5MMF+jrBaGzO+EM0GlmitHU=
github.com/xonvanetta/shutdown v0.0.3/go.mod h1:bYnVnX8ITK2E9GpuH/YVfctve/d5oOIvWsyhFj/N450=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190508220229-2d0786266e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,13 @@
package metrics
import (
"log"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
)
var (
// DomainsBlocked - The number of domains being blocked by Pi-hole.
// DomainsBlocked - The number of domains being blocked by PI-Hole.
DomainsBlocked = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "domains_being_blocked",
@ -16,7 +17,7 @@ var (
[]string{"hostname"},
)
// DNSQueriesToday - The number of DNS requests made over Pi-hole over the current day.
// DNSQueriesToday - The number of DNS requests made over PI-Hole over the current day.
DNSQueriesToday = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "dns_queries_today",
@ -26,7 +27,7 @@ var (
[]string{"hostname"},
)
// AdsBlockedToday - The number of ads blocked by Pi-hole over the current day.
// AdsBlockedToday - The number of ads blocked by PI-Hole over the current day.
AdsBlockedToday = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "ads_blocked_today",
@ -36,7 +37,7 @@ var (
[]string{"hostname"},
)
// AdsPercentageToday - The percentage of ads blocked by Pi-hole over the current day.
// AdsPercentageToday - The percentage of ads blocked by PI-Hole over the current day.
AdsPercentageToday = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "ads_percentage_today",
@ -46,7 +47,7 @@ var (
[]string{"hostname"},
)
// UniqueDomains - The number of unique domains seen by Pi-hole.
// UniqueDomains - The number of unique domains seen by PI-Hole.
UniqueDomains = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "unique_domains",
@ -56,7 +57,7 @@ var (
[]string{"hostname"},
)
// QueriesForwarded - The number of queries forwarded by Pi-hole.
// QueriesForwarded - The number of queries forwarded by PI-Hole.
QueriesForwarded = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "queries_forwarded",
@ -66,7 +67,7 @@ var (
[]string{"hostname"},
)
// QueriesCached - The number of queries cached by Pi-hole.
// QueriesCached - The number of queries cached by PI-Hole.
QueriesCached = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "queries_cached",
@ -76,7 +77,7 @@ var (
[]string{"hostname"},
)
// ClientsEverSeen - The number of clients ever seen by Pi-hole.
// ClientsEverSeen - The number of clients ever seen by PI-Hole.
ClientsEverSeen = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "clients_ever_seen",
@ -86,7 +87,7 @@ var (
[]string{"hostname"},
)
// UniqueClients - The number of unique clients seen by Pi-hole.
// UniqueClients - The number of unique clients seen by PI-Hole.
UniqueClients = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "unique_clients",
@ -96,7 +97,7 @@ var (
[]string{"hostname"},
)
// DNSQueriesAllTypes - The number of DNS queries made for all types by Pi-hole.
// DNSQueriesAllTypes - The number of DNS queries made for all types by PI-Hole.
DNSQueriesAllTypes = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "dns_queries_all_types",
@ -106,7 +107,7 @@ var (
[]string{"hostname"},
)
// Reply - The number of replies made for every types by Pi-hole.
// Reply - The number of replies made for every types by PI-Hole.
Reply = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "reply",
@ -116,68 +117,68 @@ var (
[]string{"hostname", "type"},
)
// TopQueries - The number of top queries made by Pi-hole by domain.
// TopQueries - The number of top queries made by PI-Hole by domain.
TopQueries = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "top_queries",
Namespace: "pihole",
Help: "This represent the number of top queries made by Pi-hole by domain",
Help: "This represent the number of top queries made by PI-Hole by domain",
},
[]string{"hostname", "domain"},
)
// TopAds - The number of top ads made by Pi-hole by domain.
// TopAds - The number of top ads made by PI-Hole by domain.
TopAds = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "top_ads",
Namespace: "pihole",
Help: "This represent the number of top ads made by Pi-hole by domain",
Help: "This represent the number of top ads made by PI-Hole by domain",
},
[]string{"hostname", "domain"},
)
// TopSources - The number of top sources requests made by Pi-hole by source host.
// TopSources - The number of top sources requests made by PI-Hole by source host.
TopSources = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "top_sources",
Namespace: "pihole",
Help: "This represent the number of top sources requests made by Pi-hole by source host",
Help: "This represent the number of top sources requests made by PI-Hole by source host",
},
[]string{"hostname", "source"},
)
// ForwardDestinations - The number of forward destinations requests made by Pi-hole by destination.
// ForwardDestinations - The number of forward destinations requests made by PI-Hole by destination.
ForwardDestinations = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "forward_destinations",
Namespace: "pihole",
Help: "This represent the number of forward destinations requests made by Pi-hole by destination",
Help: "This represent the number of forward destinations requests made by PI-Hole by destination",
},
[]string{"hostname", "destination"},
)
// QueryTypes - The number of queries made by Pi-hole by type.
// QueryTypes - The number of queries made by PI-Hole by type.
QueryTypes = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "querytypes",
Namespace: "pihole",
Help: "This represent the number of queries made by Pi-hole by type",
Help: "This represent the number of queries made by PI-Hole by type",
},
[]string{"hostname", "type"},
)
// Status - Is Pi-hole enabled?
// Status - Is PI-Hole enabled?
Status = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "status",
Namespace: "pihole",
Help: "This if Pi-hole is enabled",
Help: "This if PI-Hole is enabled",
},
[]string{"hostname"},
)
)
// Init initializes all Prometheus metrics made available by Pi-hole exporter.
// Init initializes all Prometheus metrics made available by PI-Hole exporter.
func Init() {
initMetric("domains_blocked", DomainsBlocked)
initMetric("dns_queries_today", DNSQueriesToday)
@ -200,5 +201,5 @@ func Init() {
func initMetric(name string, metric *prometheus.GaugeVec) {
prometheus.MustRegister(metric)
log.Info("New Prometheus metric registered: ", name)
log.Printf("New Prometheus metric registered: %s", name)
}

View file

@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
@ -11,161 +12,101 @@ import (
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/eko/pihole-exporter/config"
"github.com/eko/pihole-exporter/internal/metrics"
)
type ClientStatus byte
const (
MetricsCollectionInProgress ClientStatus = iota
MetricsCollectionSuccess
MetricsCollectionError
MetricsCollectionTimeout
var (
loginURLPattern = "http://%s/admin/index.php?login"
statsURLPattern = "http://%s/admin/api.php?summaryRaw&overTimeData&topItems&recentItems&getQueryTypes&getForwardDestinations&getQuerySources&jsonForceObject"
)
func (status ClientStatus) String() string {
return []string{"MetricsCollectionInProgress", "MetricsCollectionSuccess", "MetricsCollectionError", "MetricsCollectionTimeout"}[status]
}
type ClientChannel struct {
Status ClientStatus
Err error
}
func (c *ClientChannel) String() string {
if c.Err != nil {
return fmt.Sprintf("ClientChannel<Status: %s, Err: '%s'>", c.Status, c.Err.Error())
} else {
return fmt.Sprintf("ClientChannel<Status: %s, Err: <nil>>", c.Status)
}
}
// Client struct is a Pi-hole client to request an instance of a Pi-hole ad blocker.
// Client struct is a PI-Hole client to request an instance of a PI-Hole ad blocker.
type Client struct {
httpClient http.Client
interval time.Duration
config *config.Config
Status chan *ClientChannel
hostname string
password string
sessionID string
}
// NewClient method initializes a new Pi-hole client.
func NewClient(config *config.Config, envConfig *config.EnvConfig) *Client {
err := config.Validate()
if err != nil {
log.Error(err)
os.Exit(1)
}
log.Printf("Creating client with config %s\n", config)
// NewClient method initializes a new PI-Hole client.
func NewClient(hostname, password string, interval time.Duration) *Client {
return &Client{
config: config,
hostname: hostname,
password: password,
interval: interval,
httpClient: http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Timeout: envConfig.Timeout,
},
Status: make(chan *ClientChannel, 1),
}
}
func (c *Client) String() string {
return c.config.PIHoleHostname
}
// Scrape method logins and retrieves statistics from PI-Hole JSON API
// and then pass them as Prometheus metrics.
func (c *Client) Scrape() {
for range time.Tick(c.interval) {
if c.isAuthenticated() {
c.sessionID = c.getPHPSessionID()
}
func (c *Client) CollectMetricsAsync(writer http.ResponseWriter, request *http.Request) {
log.Printf("Collecting from %s", c.config.PIHoleHostname)
if stats, err := c.getStatistics(); err == nil {
stats := c.getStatistics()
c.setMetrics(stats)
c.Status <- &ClientChannel{Status: MetricsCollectionSuccess, Err: nil}
log.Printf("New tick of statistics from %s: %s", c.config.PIHoleHostname, stats)
} else {
c.Status <- &ClientChannel{Status: MetricsCollectionError, Err: err}
}
}
func (c *Client) CollectMetrics(writer http.ResponseWriter, request *http.Request) error {
stats, err := c.getStatistics()
if err != nil {
return err
log.Printf("New tick of statistics: %s", stats.ToString())
}
c.setMetrics(stats)
log.Printf("New tick of statistics from %s: %s", c.config.PIHoleHostname, stats)
return nil
}
func (c *Client) GetHostname() string {
return c.config.PIHoleHostname
}
func (c *Client) setMetrics(stats *Stats) {
metrics.DomainsBlocked.WithLabelValues(c.config.PIHoleHostname).Set(float64(stats.DomainsBeingBlocked))
metrics.DNSQueriesToday.WithLabelValues(c.config.PIHoleHostname).Set(float64(stats.DNSQueriesToday))
metrics.AdsBlockedToday.WithLabelValues(c.config.PIHoleHostname).Set(float64(stats.AdsBlockedToday))
metrics.AdsPercentageToday.WithLabelValues(c.config.PIHoleHostname).Set(float64(stats.AdsPercentageToday))
metrics.UniqueDomains.WithLabelValues(c.config.PIHoleHostname).Set(float64(stats.UniqueDomains))
metrics.QueriesForwarded.WithLabelValues(c.config.PIHoleHostname).Set(float64(stats.QueriesForwarded))
metrics.QueriesCached.WithLabelValues(c.config.PIHoleHostname).Set(float64(stats.QueriesCached))
metrics.ClientsEverSeen.WithLabelValues(c.config.PIHoleHostname).Set(float64(stats.ClientsEverSeen))
metrics.UniqueClients.WithLabelValues(c.config.PIHoleHostname).Set(float64(stats.UniqueClients))
metrics.DNSQueriesAllTypes.WithLabelValues(c.config.PIHoleHostname).Set(float64(stats.DNSQueriesAllTypes))
metrics.DomainsBlocked.WithLabelValues(c.hostname).Set(float64(stats.DomainsBeingBlocked))
metrics.DNSQueriesToday.WithLabelValues(c.hostname).Set(float64(stats.DNSQueriesToday))
metrics.AdsBlockedToday.WithLabelValues(c.hostname).Set(float64(stats.AdsBlockedToday))
metrics.AdsPercentageToday.WithLabelValues(c.hostname).Set(float64(stats.AdsPercentageToday))
metrics.UniqueDomains.WithLabelValues(c.hostname).Set(float64(stats.UniqueDomains))
metrics.QueriesForwarded.WithLabelValues(c.hostname).Set(float64(stats.QueriesForwarded))
metrics.QueriesCached.WithLabelValues(c.hostname).Set(float64(stats.QueriesCached))
metrics.ClientsEverSeen.WithLabelValues(c.hostname).Set(float64(stats.ClientsEverSeen))
metrics.UniqueClients.WithLabelValues(c.hostname).Set(float64(stats.UniqueClients))
metrics.DNSQueriesAllTypes.WithLabelValues(c.hostname).Set(float64(stats.DNSQueriesAllTypes))
metrics.Reply.WithLabelValues(c.config.PIHoleHostname, "unknown").Set(float64(stats.ReplyUnknown))
metrics.Reply.WithLabelValues(c.config.PIHoleHostname, "no_data").Set(float64(stats.ReplyNoData))
metrics.Reply.WithLabelValues(c.config.PIHoleHostname, "nx_domain").Set(float64(stats.ReplyNxDomain))
metrics.Reply.WithLabelValues(c.config.PIHoleHostname, "cname").Set(float64(stats.ReplyCname))
metrics.Reply.WithLabelValues(c.config.PIHoleHostname, "ip").Set(float64(stats.ReplyIP))
metrics.Reply.WithLabelValues(c.config.PIHoleHostname, "domain").Set(float64(stats.ReplyDomain))
metrics.Reply.WithLabelValues(c.config.PIHoleHostname, "rr_name").Set(float64(stats.ReplyRRName))
metrics.Reply.WithLabelValues(c.config.PIHoleHostname, "serv_fail").Set(float64(stats.ReplyServFail))
metrics.Reply.WithLabelValues(c.config.PIHoleHostname, "refused").Set(float64(stats.ReplyRefused))
metrics.Reply.WithLabelValues(c.config.PIHoleHostname, "not_imp").Set(float64(stats.ReplyNotImp))
metrics.Reply.WithLabelValues(c.config.PIHoleHostname, "other").Set(float64(stats.ReplyOther))
metrics.Reply.WithLabelValues(c.config.PIHoleHostname, "dnssec").Set(float64(stats.ReplyDNSSEC))
metrics.Reply.WithLabelValues(c.config.PIHoleHostname, "none").Set(float64(stats.ReplyNone))
metrics.Reply.WithLabelValues(c.config.PIHoleHostname, "blob").Set(float64(stats.ReplyBlob))
metrics.Reply.WithLabelValues(c.hostname, "no_data").Set(float64(stats.ReplyNoData))
metrics.Reply.WithLabelValues(c.hostname, "nx_domain").Set(float64(stats.ReplyNxDomain))
metrics.Reply.WithLabelValues(c.hostname, "cname").Set(float64(stats.ReplyCname))
metrics.Reply.WithLabelValues(c.hostname, "ip").Set(float64(stats.ReplyIP))
var isEnabled int = 0
if stats.Status == enabledStatus {
isEnabled = 1
}
metrics.Status.WithLabelValues(c.config.PIHoleHostname).Set(float64(isEnabled))
// Pi-hole returns a subset of stats when Auth is missing or incorrect.
// This provides a warning to users that metrics are not complete.
if len(stats.TopQueries) == 0 {
log.Warnf("Invalid Authentication - Some metrics may be missing. Please confirm your Pi-hole API token / Password for %s", c.config.PIHoleHostname)
}
metrics.Status.WithLabelValues(c.hostname).Set(float64(isEnabled))
for domain, value := range stats.TopQueries {
metrics.TopQueries.WithLabelValues(c.config.PIHoleHostname, domain).Set(float64(value))
metrics.TopQueries.WithLabelValues(c.hostname, domain).Set(float64(value))
}
for domain, value := range stats.TopAds {
metrics.TopAds.WithLabelValues(c.config.PIHoleHostname, domain).Set(float64(value))
metrics.TopAds.WithLabelValues(c.hostname, domain).Set(float64(value))
}
for source, value := range stats.TopSources {
metrics.TopSources.WithLabelValues(c.config.PIHoleHostname, source).Set(float64(value))
metrics.TopSources.WithLabelValues(c.hostname, source).Set(float64(value))
}
for destination, value := range stats.ForwardDestinations {
metrics.ForwardDestinations.WithLabelValues(c.config.PIHoleHostname, destination).Set(value)
metrics.ForwardDestinations.WithLabelValues(c.hostname, destination).Set(value)
}
for queryType, value := range stats.QueryTypes {
metrics.QueryTypes.WithLabelValues(c.config.PIHoleHostname, queryType).Set(value)
metrics.QueryTypes.WithLabelValues(c.hostname, queryType).Set(value)
}
}
func (c *Client) getPHPSessionID() (sessionID string) {
values := url.Values{"pw": []string{c.config.PIHolePassword}}
loginURL := fmt.Sprintf(loginURLPattern, c.hostname)
values := url.Values{"pw": []string{c.password}}
req, err := http.NewRequest("POST", c.config.PIHoleLoginURL(), strings.NewReader(values.Encode()))
req, err := http.NewRequest("POST", loginURL, strings.NewReader(values.Encode()))
if err != nil {
log.Fatal("An error has occured when creating HTTP statistics request", err)
}
@ -175,7 +116,12 @@ func (c *Client) getPHPSessionID() (sessionID string) {
resp, err := c.httpClient.Do(req)
if err != nil {
log.Errorf("An error has occured during login to Pi-hole: %v", err)
log.Printf("An error has occured during login to PI-Hole: %v", err)
}
if resp.StatusCode != http.StatusFound {
log.Printf("Unable to login to PI-Hole, got a HTTP status code response '%d' instead of '%d'", resp.StatusCode, http.StatusFound)
os.Exit(1)
}
for _, cookie := range resp.Cookies() {
@ -188,52 +134,43 @@ func (c *Client) getPHPSessionID() (sessionID string) {
return
}
func (c *Client) getStatistics() (*Stats, error) {
stats := new(Stats)
func (c *Client) getStatistics() *Stats {
var stats Stats
statsURL := c.config.PIHoleStatsURL()
if c.isUsingApiToken() {
statsURL = fmt.Sprintf("%s&auth=%s", statsURL, c.config.PIHoleApiToken)
}
statsURL := fmt.Sprintf(statsURLPattern, c.hostname)
req, err := http.NewRequest("GET", statsURL, nil)
if err != nil {
return nil, fmt.Errorf("an error has occured when creating HTTP statistics request: %w", err)
log.Fatal("An error has occured when creating HTTP statistics request", err)
}
if c.isUsingPassword() {
if c.isAuthenticated() {
c.authenticateRequest(req)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("an error has occured during retrieving Pi-hole statistics: %w", err)
log.Println("An error has occured during retrieving PI-Hole statistics", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("unable to read Pi-hole statistics HTTP response: %w", err)
log.Println("Unable to read PI-Hole statistics HTTP response", err)
}
err = json.Unmarshal(body, stats)
err = json.Unmarshal(body, &stats)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal Pi-hole statistics to statistics struct model: %w", err)
log.Println("Unable to unmarshal PI-Hole statistics to statistics struct model", err)
}
return stats, nil
return &stats
}
func (c *Client) isUsingPassword() bool {
return len(c.config.PIHolePassword) > 0
}
func (c *Client) isUsingApiToken() bool {
return len(c.config.PIHoleApiToken) > 0
func (c *Client) isAuthenticated() bool {
return len(c.password) > 0
}
func (c *Client) authenticateRequest(req *http.Request) {
cookie := http.Cookie{Name: "PHPSESSID", Value: c.getPHPSessionID()}
cookie := http.Cookie{Name: "PHPSESSID", Value: c.sessionID}
req.AddCookie(&cookie)
}

View file

@ -6,7 +6,7 @@ const (
enabledStatus = "enabled"
)
// Stats struct is the Pi-hole statistics JSON API corresponding model.
// Stats struct is the PI-Hole statistics JSON API corresponding model.
type Stats struct {
DomainsBeingBlocked int `json:"domains_being_blocked"`
DNSQueriesToday int `json:"dns_queries_today"`
@ -18,20 +18,10 @@ type Stats struct {
ClientsEverSeen int `json:"clients_ever_seen"`
UniqueClients int `json:"unique_clients"`
DNSQueriesAllTypes int `json:"dns_queries_all_types"`
ReplyUnknown int `json:"reply_UNKNOWN"`
ReplyNoData int `json:"reply_NODATA"`
ReplyNxDomain int `json:"reply_NXDOMAIN"`
ReplyCname int `json:"reply_CNAME"`
ReplyIP int `json:"reply_IP"`
ReplyDomain int `json:"reply_DOMAIN"`
ReplyRRName int `json:"reply_RRNAME"`
ReplyServFail int `json:"reply_SERVFAIL"`
ReplyRefused int `json:"reply_REFUSED"`
ReplyNotImp int `json:"reply_NOTIMP"`
ReplyOther int `json:"reply_OTHER"`
ReplyDNSSEC int `json:"reply_DNSSEC"`
ReplyNone int `json:"reply_NONE"`
ReplyBlob int `json:"reply_BLOB"`
TopQueries map[string]int `json:"top_queries"`
TopAds map[string]int `json:"top_ads"`
TopSources map[string]int `json:"top_sources"`
@ -41,6 +31,6 @@ type Stats struct {
}
// ToString method returns a string of the current statistics struct.
func (s *Stats) String() string {
func (s *Stats) ToString() string {
return fmt.Sprintf("%d ads blocked / %d total DNS queries", s.AdsBlockedToday, s.DNSQueriesAllTypes)
}

View file

@ -1,15 +1,11 @@
package server
import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/eko/pihole-exporter/internal/pihole"
"github.com/prometheus/client_golang/prometheus/promhttp"
log "github.com/sirupsen/logrus"
"golang.org/x/net/context"
)
@ -20,34 +16,15 @@ type Server struct {
// NewServer method initializes a new HTTP server instance and associates
// the different routes that will be used by Prometheus (metrics) or for monitoring (readiness, liveness).
func NewServer(addr string, port uint16, clients []*pihole.Client) *Server {
func NewServer(port string) *Server {
mux := http.NewServeMux()
httpServer := &http.Server{
Addr: addr + ":" + strconv.Itoa(int(port)),
Handler: mux,
}
httpServer := &http.Server{Addr: ":" + port, Handler: mux}
s := &Server{
httpServer: httpServer,
}
mux.HandleFunc("/metrics", func(writer http.ResponseWriter, request *http.Request) {
log.Printf("request.Header: %v\n", request.Header)
for _, client := range clients {
go client.CollectMetricsAsync(writer, request)
}
for _, client := range clients {
status := <-client.Status
if status.Status == pihole.MetricsCollectionError {
log.Printf("An error occured while contacting %s: %s", client.GetHostname(), status.Err.Error())
}
}
promhttp.Handler().ServeHTTP(writer, request)
})
mux.Handle("/metrics", promhttp.Handler())
mux.Handle("/readiness", s.readinessHandler())
mux.Handle("/liveness", s.livenessHandler())
@ -72,41 +49,20 @@ func (s *Server) Stop() {
s.httpServer.Shutdown(ctx)
}
func (s *Server) handleMetrics(clients []*pihole.Client) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
errors := make([]string, 0)
for _, client := range clients {
if err := client.CollectMetrics(writer, request); err != nil {
errors = append(errors, err.Error())
fmt.Printf("Error %s\n", err)
}
}
if len(errors) == len(clients) {
writer.WriteHeader(http.StatusBadRequest)
body := strings.Join(errors, "\n")
_, _ = writer.Write([]byte(body))
}
promhttp.Handler().ServeHTTP(writer, request)
}
}
func (s *Server) readinessHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if s.isReady() {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusNotFound)
}
}
})
}
func (s *Server) livenessHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
}
})
}
func (s *Server) isReady() bool {

72
main.go
View file

@ -1,55 +1,53 @@
package main
import (
"log"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/eko/pihole-exporter/config"
"github.com/eko/pihole-exporter/internal/metrics"
"github.com/eko/pihole-exporter/internal/pihole"
"github.com/eko/pihole-exporter/internal/server"
"github.com/xonvanetta/shutdown/pkg/shutdown"
)
const (
name = "pihole-exporter"
)
var (
s *server.Server
)
func main() {
envConf, clientConfigs, err := config.Load()
if err != nil {
log.Fatal(err.Error())
}
conf := config.Load()
metrics.Init()
serverDead := make(chan struct{})
initPiholeClient(conf.PIHoleHostname, conf.PIHolePassword, conf.Interval)
initHttpServer(conf.Port)
clients := buildClients(clientConfigs, envConf)
s := server.NewServer(envConf.BindAddr, envConf.Port, clients)
go func() {
s.ListenAndServe()
close(serverDead)
}()
ctx := shutdown.Context()
go func() {
<-ctx.Done()
s.Stop()
}()
select {
case <-ctx.Done():
case <-serverDead:
}
log.Println("pihole-exporter HTTP server stopped")
handleExitSignal()
}
func buildClients(clientConfigs []config.Config, envConfig *config.EnvConfig) []*pihole.Client {
clients := make([]*pihole.Client, 0, len(clientConfigs))
for i := range clientConfigs {
clientConfig := &clientConfigs[i]
client := pihole.NewClient(clientConfig, envConfig)
clients = append(clients, client)
}
return clients
func initPiholeClient(hostname, password string, interval time.Duration) {
client := pihole.NewClient(hostname, password, interval)
go client.Scrape()
}
func initHttpServer(port string) {
s = server.NewServer(port)
go s.ListenAndServe()
}
func handleExitSignal() {
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
s.Stop()
fmt.Println(fmt.Sprintf("\n%s HTTP server stopped", name))
}