Compare commits
No commits in common. "main" and "feat/telegraf-snmp-unifi-own-dockerfile" have entirely different histories.
main
...
feat/teleg
@ -1,69 +0,0 @@
|
|||||||
name: Build and push adguard-exporter image
|
|
||||||
run-name: ${{ gitea.actor }} is building and pushing adguard-exporter image
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- 'feat/**'
|
|
||||||
paths:
|
|
||||||
- 'adguard-exporter/**'
|
|
||||||
- '.gitea/workflows/adguard-exporter.yaml'
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
RESULT_IMAGE_NAME: tools/adguard-exporter
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push-image:
|
|
||||||
runs-on: docker
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Log in to registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ vars.PRIVATE_REGISTRY_DOMAIN }}
|
|
||||||
username: ${{ vars.PRIVATE_REGISTRY_USER }}
|
|
||||||
password: ${{ secrets.PRIVATE_REGISTRY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Install private CA certificate
|
|
||||||
run: |
|
|
||||||
echo "${{ vars.PRIVATE_REGISTRY_CA_CERT }}" | sudo tee /usr/local/share/ca-certificates/private-ca.crt > /dev/null
|
|
||||||
sudo update-ca-certificates
|
|
||||||
|
|
||||||
- name: Compute image tags
|
|
||||||
id: tags
|
|
||||||
run: |
|
|
||||||
BASE="${{ vars.PRIVATE_REGISTRY_DOMAIN }}/${{ env.RESULT_IMAGE_NAME }}"
|
|
||||||
{
|
|
||||||
echo "value<<EOF"
|
|
||||||
echo "${BASE}:${{ gitea.sha }}"
|
|
||||||
if [ "${{ gitea.ref_name }}" = "main" ]; then
|
|
||||||
echo "${BASE}:latest"
|
|
||||||
fi
|
|
||||||
if [[ "${{ gitea.ref }}" == refs/tags/v* ]]; then
|
|
||||||
echo "${BASE}:${{ gitea.ref_name }}"
|
|
||||||
fi
|
|
||||||
echo "EOF"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
with:
|
|
||||||
config-inline: |
|
|
||||||
[registry."harbor.lan"]
|
|
||||||
ca = ["/usr/local/share/ca-certificates/private-ca.crt"]
|
|
||||||
|
|
||||||
- name: Build and push image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: adguard-exporter
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.tags.outputs.value }}
|
|
||||||
@ -6,7 +6,6 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `tools/telegraf-snmp-unifi` | Telegraf mit UniFi SNMP MIBs |
|
| `tools/telegraf-snmp-unifi` | Telegraf mit UniFi SNMP MIBs |
|
||||||
| `tools/rsyncd` | rsync Daemon |
|
| `tools/rsyncd` | rsync Daemon |
|
||||||
| `tools/adguard-exporter` | Prometheus Exporter für AdGuard Home — basiert auf [znand-dev/adguardexporter](https://github.com/znand-dev/adguardexporter), GeoLite2 deaktiviert (kein DB-File) |
|
|
||||||
|
|
||||||
## CI Workflows
|
## CI Workflows
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
# Contoh konfigurasi, silakan sesuaikan
|
|
||||||
#Fill up your adguard url, if didn't works try docker ips (172.x.x.x) (required)
|
|
||||||
ADGUARD_HOST=http://172.x.x.x:20271
|
|
||||||
|
|
||||||
#Your AdGuard username and password for authentication that you setup during the init (required)
|
|
||||||
ADGUARD_USER=
|
|
||||||
ADGUARD_PASS=
|
|
||||||
|
|
||||||
#Your exporter port (optional, default 9617)
|
|
||||||
EXPORTER_PORT=9200
|
|
||||||
|
|
||||||
#Scrape Interval by prometheus (optional, default 15s)
|
|
||||||
SCRAPE_INTERVAL=15s
|
|
||||||
|
|
||||||
#Fill up your LogLevel you want it to, (DEBUG,WARN,ERROR,INFO)
|
|
||||||
LOG_LEVEL=DEBUG
|
|
||||||
|
|
||||||
#GEOIP_DB File download => wget https://raw.githubusercontent.com/P3TERX/GeoLite.mmdb/download/GeoLite2-City.mmdb
|
|
||||||
GEOIP_DB=/path/to/your/GeoLite2-City.mmdb
|
|
||||||
29
adguard-exporter/.github/renovate.json
vendored
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": [
|
|
||||||
"config:recommended"
|
|
||||||
],
|
|
||||||
"labels": [
|
|
||||||
"renovate::dependencies",
|
|
||||||
"{{#if category}}renovate::{{category}}{{/if}}",
|
|
||||||
"{{#if updateType}}renovate::{{updateType}}{{/if}}",
|
|
||||||
"{{#if datasource}}renovate::{{datasource}}{{/if}}",
|
|
||||||
"{{#if manager}}renovate::{{manager}}{{/if}}",
|
|
||||||
"{{#if vulnerabilitySeverity}}renovate::{{vulnerabilitySeverity}}{{/if}}",
|
|
||||||
"renovate::{{#if isVulnerabilityAlert}}vulnerability{{else}}not-vulnerability{{/if}}"
|
|
||||||
],
|
|
||||||
"pinDigests": false,
|
|
||||||
"enabled": true,
|
|
||||||
"separateMajorMinor": true,
|
|
||||||
"automerge": true,
|
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"matchManagers": [
|
|
||||||
"gomod"
|
|
||||||
],
|
|
||||||
"postUpdateOptions": [
|
|
||||||
"gomodTidy"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
44
adguard-exporter/.github/workflows/go-ci.yml
vendored
@ -1,44 +0,0 @@
|
|||||||
name: Go CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main" ]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: Run Tests & Linter
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version: "1.26"
|
|
||||||
|
|
||||||
# - name: Cache Go modules
|
|
||||||
# uses: actions/cache@v3
|
|
||||||
# with:
|
|
||||||
# path: |
|
|
||||||
# ~/.cache/go-build
|
|
||||||
# ~/go/pkg/mod
|
|
||||||
# key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
# restore-keys: |
|
|
||||||
# ${{ runner.os }}-go-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: go mod tidy
|
|
||||||
|
|
||||||
- name: Run unit tests
|
|
||||||
run: go test -v ./...
|
|
||||||
|
|
||||||
- name: Run linter (golangci-lint)
|
|
||||||
uses: golangci/golangci-lint-action@v9
|
|
||||||
with:
|
|
||||||
version: latest
|
|
||||||
args: --timeout=3m
|
|
||||||
32
adguard-exporter/.github/workflows/release.yml
vendored
@ -1,32 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout source
|
|
||||||
uses: actions/checkout@v7
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v6
|
|
||||||
with:
|
|
||||||
go-version: 1.26
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
run: echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u znand-dev --password-stdin
|
|
||||||
- name: Run GoReleaser
|
|
||||||
uses: goreleaser/goreleaser-action@v7
|
|
||||||
with:
|
|
||||||
version: latest
|
|
||||||
args: release --clean
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
13
adguard-exporter/.gitignore
vendored
@ -1,13 +0,0 @@
|
|||||||
# Binaries
|
|
||||||
adguardexporter
|
|
||||||
dist/
|
|
||||||
# Env file
|
|
||||||
.env
|
|
||||||
|
|
||||||
# OS / Editor files
|
|
||||||
.DS_Store
|
|
||||||
*.swp
|
|
||||||
|
|
||||||
#db
|
|
||||||
GeoLite2-City.mmdb
|
|
||||||
geolite2-db/GeoLite2-City.mmdb
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
project_name: adguard-exporter
|
|
||||||
|
|
||||||
before:
|
|
||||||
hooks:
|
|
||||||
- go mod tidy
|
|
||||||
|
|
||||||
builds:
|
|
||||||
- id: universal
|
|
||||||
main: main.go
|
|
||||||
binary: adguard-exporter
|
|
||||||
env: [CGO_ENABLED=0]
|
|
||||||
goos:
|
|
||||||
- linux
|
|
||||||
- windows
|
|
||||||
- darwin
|
|
||||||
goarch:
|
|
||||||
- amd64
|
|
||||||
- arm64
|
|
||||||
- arm
|
|
||||||
|
|
||||||
archives:
|
|
||||||
- format: tar.gz
|
|
||||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
|
||||||
files:
|
|
||||||
- LICENSE
|
|
||||||
- README.md
|
|
||||||
|
|
||||||
checksum:
|
|
||||||
name_template: 'checksums.txt'
|
|
||||||
|
|
||||||
dockers:
|
|
||||||
- id: docker-amd64
|
|
||||||
ids: [universal]
|
|
||||||
goos: linux
|
|
||||||
goarch: amd64
|
|
||||||
use: buildx
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/amd64"
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/znand-dev/adguardexporter:{{ .Tag }}-amd64
|
|
||||||
- ghcr.io/znand-dev/adguardexporter:latest-amd64
|
|
||||||
|
|
||||||
- id: docker-arm64
|
|
||||||
ids: [universal]
|
|
||||||
goos: linux
|
|
||||||
goarch: arm64
|
|
||||||
use: buildx
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/arm64"
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/znand-dev/adguardexporter:{{ .Tag }}-arm64v8
|
|
||||||
- ghcr.io/znand-dev/adguardexporter:latest-arm64v8
|
|
||||||
|
|
||||||
- id: docker-arm
|
|
||||||
ids: [universal]
|
|
||||||
goos: linux
|
|
||||||
goarch: arm
|
|
||||||
use: buildx
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--platform=linux/arm/v6"
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/znand-dev/adguardexporter:{{ .Tag }}-armv6
|
|
||||||
- ghcr.io/znand-dev/adguardexporter:latest-armv6
|
|
||||||
|
|
||||||
docker_manifests:
|
|
||||||
- name_template: ghcr.io/znand-dev/adguardexporter:{{ .Tag }}
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/znand-dev/adguardexporter:{{ .Tag }}-amd64
|
|
||||||
- ghcr.io/znand-dev/adguardexporter:{{ .Tag }}-arm64v8
|
|
||||||
- ghcr.io/znand-dev/adguardexporter:{{ .Tag }}-armv6
|
|
||||||
|
|
||||||
- name_template: ghcr.io/znand-dev/adguardexporter:latest
|
|
||||||
image_templates:
|
|
||||||
- ghcr.io/znand-dev/adguardexporter:latest-amd64
|
|
||||||
- ghcr.io/znand-dev/adguardexporter:latest-arm64v8
|
|
||||||
- ghcr.io/znand-dev/adguardexporter:latest-armv6
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
## v1.1.0 - 2025-06-18
|
|
||||||
### ✨ Added
|
|
||||||
- `adguard_replaced_parental`: total queries replaced due to parental control
|
|
||||||
- `adguard_safe_search`: total queries forced into Safe Search (YouTube, Google, Bing)
|
|
||||||
- `adguard_safe_browsing`: total Safe Browsing protections triggered
|
|
||||||
- `adguard_top_upstreams`: top DNS upstreams based on query count
|
|
||||||
- `adguard_top_clients`: top client IPs querying AdGuard
|
|
||||||
- `adguard_top_queried_domains`: most frequently queried domains
|
|
||||||
- `adguard_upstream_avg_response_time_seconds`: average DNS upstream response time
|
|
||||||
|
|
||||||
### 🛠 Fixed
|
|
||||||
- Fixed: missing `upstream` and `client` labels due to empty maps or parsing errors
|
|
||||||
- Fixed: more reliable parsing of nested JSON from `/control/stats` and `/control/status`
|
|
||||||
- Fixed: exporter now exposes partial metrics even if part of the JSON fails
|
|
||||||
|
|
||||||
### 🔄 Refactored
|
|
||||||
- Cleaned up metric and label names to follow Prometheus naming conventions
|
|
||||||
- Fixed Dockerfile lint warning: unified casing of `FROM ... AS` keyword for best practices
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS build
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates git
|
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
ARG TARGETOS
|
|
||||||
ARG TARGETARCH
|
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
|
|
||||||
go build -ldflags="-s -w" -o adguard-exporter .
|
|
||||||
|
|
||||||
# Create empty GeoLite2 placeholder (geo metrics disabled without real DB)
|
|
||||||
RUN touch /app/GeoLite2-City.mmdb
|
|
||||||
|
|
||||||
FROM alpine:3.24 AS certs
|
|
||||||
RUN apk add --no-cache ca-certificates
|
|
||||||
|
|
||||||
FROM scratch
|
|
||||||
WORKDIR /
|
|
||||||
|
|
||||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
|
||||||
COPY --from=build /app/adguard-exporter /adguard-exporter
|
|
||||||
COPY --from=build /app/GeoLite2-City.mmdb /GeoLite2-City.mmdb
|
|
||||||
|
|
||||||
USER 65532:65532
|
|
||||||
|
|
||||||
EXPOSE 9200
|
|
||||||
|
|
||||||
ENTRYPOINT ["/adguard-exporter"]
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 Januar Ismunandar
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
@ -1,307 +0,0 @@
|
|||||||
# AdGuard Exporter for Prometheus
|
|
||||||
|
|
||||||
A lightweight Prometheus exporter written in Go that exposes detailed metrics from your AdGuard Home instance — including DNS statistics, blocked domains, upstreams, and client data.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
| Category | Capabilities |
|
|
||||||
|----------|--------------|
|
|
||||||
| API Integration | • Authenticated access to AdGuard Home API<br>• Supports `/control/status`, `/control/stats`, and `/control/querylog` endpoints |
|
|
||||||
| DNS Analytics | • Total DNS queries metrics<br>• Blocked DNS queries tracking<br>• Upstream DNS statistics<br>• Per-client query statistics<br>• Query reason statistics<br>• DNS upstream latency histograms |
|
|
||||||
| Query Log Processing | • Real-time query log analysis<br>• Query deduplication to prevent metric inflation<br>• Domain and query reason tracking |
|
|
||||||
| GeoIP Enrichment | • MaxMind GeoLite2 IP geolocation lookup<br>• DNS client geographic visualization (Grafana Geomap)<br>• DNS threat map for blocked queries<br>• GeoIP caching for improved performance |
|
|
||||||
| Traffic Analytics | • DNS queries grouped by ISP organization<br>• Country-level DNS traffic insights<br>• ISP-based DNS traffic monitoring |
|
|
||||||
| Exporter Monitoring | • Deduplication cache metrics<br>• GeoIP cache usage monitoring<br>• Deduplication hit counters<br>• Exporter scrape performance metrics |
|
|
||||||
| Deployment | • Customizable scrape interval<br>• Lightweight single binary deployment<br>• Docker-friendly deployment |
|
|
||||||
---
|
|
||||||
|
|
||||||
## Built With
|
|
||||||
|
|
||||||
- [Go (Golang)](https://golang.org/)
|
|
||||||
- [Prometheus Client Library](https://github.com/prometheus/client_golang)
|
|
||||||
- [AdGuard Home](https://github.com/AdguardTeam/AdGuardHome)
|
|
||||||
|
|
||||||
---
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
Before running this exporter, make sure:
|
|
||||||
|
|
||||||
- AdGuard Home is up and running
|
|
||||||
- You have a valid AdGuard username & password (without special character (!,@,`,* etc.))
|
|
||||||
- Prometheus is configured to scrape this exporter
|
|
||||||
- Docker installed (or alternatively Go 1.20+ for building from source)
|
|
||||||
- Port `9617` is available on your system
|
|
||||||
|
|
||||||
To make sure the endpoint is valid, you can check the endpoints via curl:
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -v -u <yourusername>:<yourpassword> <ADGUARD_URL>:<PORT>/control/stats
|
|
||||||
curl -v -u <yourusername>:<yourpassword> <ADGUARD_URL>:<PORT>/control/status
|
|
||||||
curl -v -u <yourusername>:<yourpassword> <ADGUARD_URL>:<PORT>/control/querylog
|
|
||||||
```
|
|
||||||
After that, you should got the output like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
* Trying 172.31.20.12:80...
|
|
||||||
* Connected to 172.31.20.12 (172.31.20.12) port 80 (#0)
|
|
||||||
* Server auth using Basic with user 'admin'
|
|
||||||
> GET /control/stats HTTP/1.1
|
|
||||||
> Host: 172.31.20.12
|
|
||||||
> Authorization: Basic Xxxxxxxxxxxxxxxxxxxxxxxxx
|
|
||||||
> User-Agent: curl/7.88.1
|
|
||||||
> Accept: */*
|
|
||||||
>
|
|
||||||
< HTTP/1.1 200 OK
|
|
||||||
< Access-Control-Allow-Origin: http://172.31.20.12
|
|
||||||
< Content-Type: application/json
|
|
||||||
< Server: AdGuardHome/v0.107.62
|
|
||||||
< Vary: Origin
|
|
||||||
< Vary: Accept-Encoding
|
|
||||||
< Date: Tue, 08 Jul 2025 16:45:56 GMT
|
|
||||||
< Transfer-Encoding: chunked
|
|
||||||
<
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
---
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
| Variable | Description | Required | Example |
|
|
||||||
|------------------|---------------------------- | -------------------|-------------------
|
|
||||||
| `ADGUARD_HOST`| AdGuard Home base URL | ✅ | `http://192.168.1.1:3000,` |
|
|
||||||
| `ADGUARD_USER`| AdGuard Home username | ✅ | `admin` |
|
|
||||||
| `ADGUARD_PASS`| AdGuard Home password | ✅ | `secretpassword` |
|
|
||||||
| `EXPORTER_PORT` | Port to expose metrics (default: 9617) | ❌ | `9200` |
|
|
||||||
| `SCRAPE_INTERVAL` | How often to scrape (default: 15s) | ❌ | `30s` |
|
|
||||||
| `LOG_LEVEL` | Log Level to analyze, INFO, WARN, DEBUG | ❌ | `DEBUG`,`WARN`,`INFO` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Quick Start
|
|
||||||
|
|
||||||
## Run via Docker Compose
|
|
||||||
|
|
||||||
### 1. Create docker-compose.yml in your root dir
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
adguard-exporter:
|
|
||||||
image: ghcr.io/znandev/adguardexporter:latest
|
|
||||||
container_name: adguard_exporter
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "9617:9617"
|
|
||||||
volumes:
|
|
||||||
- ./<path/to/your>/GeoLite2-City.mmdb:/GeoLite2-City.mmdb
|
|
||||||
environment:
|
|
||||||
- ADGUARD_HOST=<YOUR_ADGUARD_URL>
|
|
||||||
- ADGUARD_USER=<YOUR_ADGUARD_USERNAME>
|
|
||||||
- ADGUARD_PASS=<YOUR_ADGUARD_PASSWORD>
|
|
||||||
- EXPORTER_PORT=9617
|
|
||||||
- SCRAPE_INTERVAL=15s
|
|
||||||
- LOG_LEVEL=DEBUG
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Run with Docker Compose
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Metrics Endpoint
|
|
||||||
|
|
||||||
Once running, your exporter will be available at:
|
|
||||||
|
|
||||||
```
|
|
||||||
http://<host>:9617/metrics
|
|
||||||
```
|
|
||||||
|
|
||||||
Ready to scrape by Prometheus!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Example Prometheus Job
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- job_name: 'adguard-exporter'
|
|
||||||
scrape_interval: 15s
|
|
||||||
static_configs:
|
|
||||||
- targets: ['adguard-exporter:9617']
|
|
||||||
```
|
|
||||||
---
|
|
||||||
## Available Prometheus Metrics
|
|
||||||
|
|
||||||
This exporter exposes the following metrics from AdGuard Home.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Core Metrics
|
|
||||||
|
|
||||||
* `adguard_dns_queries_total`
|
|
||||||
Total DNS queries processed by AdGuard Home.
|
|
||||||
|
|
||||||
* `adguard_blocked_filtering_total`
|
|
||||||
Total DNS queries blocked by filtering rules.
|
|
||||||
|
|
||||||
* `adguard_replaced_parental`
|
|
||||||
Queries replaced by parental filtering.
|
|
||||||
|
|
||||||
* `adguard_avg_processing_time`
|
|
||||||
Average DNS query processing time in milliseconds.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### AdGuard Status
|
|
||||||
|
|
||||||
* `adguard_protection_enabled`
|
|
||||||
Whether DNS filtering protection is enabled (1 = enabled, 0 = disabled).
|
|
||||||
|
|
||||||
* `adguard_running`
|
|
||||||
Whether the AdGuard Home DNS service is running.
|
|
||||||
|
|
||||||
* `adguard_dhcp_available`
|
|
||||||
Indicates if DHCP functionality is available.
|
|
||||||
|
|
||||||
* `adguard_protection_disabled_duration_seconds`
|
|
||||||
Time since DNS protection was disabled.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Top Queries
|
|
||||||
|
|
||||||
Metrics with labels:
|
|
||||||
|
|
||||||
* `adguard_top_queried_domain_total{domain="example.com"}`
|
|
||||||
* `adguard_top_blocked_domain_total{domain="ads.example.com"}`
|
|
||||||
* `adguard_top_client_total{client="192.168.1.2"}`
|
|
||||||
* `adguard_top_upstream_total{upstream="8.8.8.8"}`
|
|
||||||
* `adguard_upstream_avg_response_time_seconds{upstream="8.8.8.8"}`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Query Log Metrics
|
|
||||||
|
|
||||||
Metrics generated from AdGuard query log analysis.
|
|
||||||
|
|
||||||
* `adguard_query_domain_total{domain="example.com"}`
|
|
||||||
* `adguard_query_reason_total{reason="FilteredBlackList"}`
|
|
||||||
* `adguard_query_type_total{type="A"}`
|
|
||||||
* `adguard_query_upstream_total{upstream="8.8.8.8"}`
|
|
||||||
* `adguard_query_client_reason_total{client="192.168.1.10",reason="FilteredBlackList"}`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Upstream Latency Metrics
|
|
||||||
|
|
||||||
Histogram metrics that track DNS upstream response time distribution.
|
|
||||||
|
|
||||||
* `adguard_upstream_latency_seconds`
|
|
||||||
|
|
||||||
This metric allows latency percentile calculations such as **p95 or p99 upstream DNS latency**.
|
|
||||||
|
|
||||||
Generated metrics:
|
|
||||||
|
|
||||||
* `adguard_upstream_latency_seconds_bucket`
|
|
||||||
* `adguard_upstream_latency_seconds_sum`
|
|
||||||
* `adguard_upstream_latency_seconds_count`
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```
|
|
||||||
adguard_upstream_latency_seconds_bucket{upstream="1.1.1.1",le="0.005"} 50
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### GeoIP Metrics
|
|
||||||
|
|
||||||
#### `adguard_client_geo_queries`
|
|
||||||
|
|
||||||
Total DNS queries per client enriched with geographic information.
|
|
||||||
|
|
||||||
Labels:
|
|
||||||
|
|
||||||
* `client` – Client IP address
|
|
||||||
* `country` – ISO country code
|
|
||||||
* `lat` – Latitude
|
|
||||||
* `lon` – Longitude
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```
|
|
||||||
adguard_client_geo_queries{client="118.99.94.204",country="ID",lat="-2.969000",lon="104.744200"} 1000
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### `adguard_blocked_geo_queries`
|
|
||||||
|
|
||||||
Counts DNS queries that were blocked by filtering rules and enriches them with geographic information.
|
|
||||||
|
|
||||||
Labels:
|
|
||||||
|
|
||||||
* `client`
|
|
||||||
* `country`
|
|
||||||
* `lat`
|
|
||||||
* `lon`
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```
|
|
||||||
adguard_blocked_geo_queries{client="118.99.94.204",country="ID",lat="-2.969000",lon="104.744200"} 306
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Exporter Health Metrics
|
|
||||||
|
|
||||||
Metrics used to monitor the exporter itself.
|
|
||||||
|
|
||||||
* `adguard_exporter_up` — Indicates if the exporter successfully scraped the AdGuard API.
|
|
||||||
* `adguard_exporter_scrape_duration_seconds` — Time taken for the exporter to scrape AdGuard API data.
|
|
||||||
* `adguard_exporter_scrape_errors_total` — Total number of errors encountered during scraping.
|
|
||||||
* `adguard_exporter_query_cache_size` — Number of cached DNS query entries used for deduplication.
|
|
||||||
* `adguard_exporter_geo_cache_size` — Number of cached GeoIP lookup entries.
|
|
||||||
* `adguard_exporter_dedup_hits_total` — Total number of duplicate DNS queries skipped by the exporter.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||
Grafana Dashboard Preview
|
|
||||||
|
|
||||||
[Live preview and monitoring, click here](https://stats.znand.my.id/public-dashboards/a0b05deb37464a17b01cbc69eb65ac5a?orgId=1&refresh=5s)
|
|
||||||
|
|
||||||
[Deploy yours here](https://grafana.com/grafana/dashboards/23579-adguard-metrics-statistics/)
|
|
||||||
— new dashboard.json for grafana will be uploaded soon
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💬 Feedback
|
|
||||||
|
|
||||||
Pull Requests, Issues, and Suggestions are always welcome!
|
|
||||||
|
|
||||||
**Made with ❤️ by [znanddev](https://github.com/znand-dev)**
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
adguard-exporter:
|
|
||||||
image: ghcr.io/znand-dev/adguardexporter:latest
|
|
||||||
container_name: adguard_exporter
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- "9617:9617"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- ./<path/to/your>/GeoLite2-City.mmdb:/GeoLite2-City.mmdb # your geolite2 db file
|
|
||||||
|
|
||||||
environment:
|
|
||||||
- ADGUARD_HOST=<ADGUARD_URL> # try docker ips if didn't works http://172.xx.x.x for example
|
|
||||||
- ADGUARD_USER=<ADGUARD_USERNAME> # required
|
|
||||||
- ADGUARD_PASS=<ADGUARD_PASSWORD> # required
|
|
||||||
- EXPORTER_PORT=9200 # optional, default 9617
|
|
||||||
- SCRAPE_INTERVAL=15s # optional
|
|
||||||
- LOG_LEVEL=DEBUG # optional
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
Download GeoLite2 City database from MaxMind:
|
|
||||||
|
|
||||||
https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
|
|
||||||
|
|
||||||
Place the file here:
|
|
||||||
|
|
||||||
geoip/GeoLite2-City.mmdb
|
|
||||||
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
module github.com/znand-dev/adguardexporter
|
|
||||||
|
|
||||||
go 1.24.0
|
|
||||||
|
|
||||||
toolchain go1.26.4
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/joho/godotenv v1.5.1
|
|
||||||
github.com/oschwald/geoip2-golang v1.13.0
|
|
||||||
github.com/prometheus/client_golang v1.23.2
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
|
||||||
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
|
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
|
||||||
github.com/prometheus/common v0.66.1 // indirect
|
|
||||||
github.com/prometheus/procfs v0.16.1 // indirect
|
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.8 // indirect
|
|
||||||
)
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
|
||||||
github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
|
|
||||||
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
|
||||||
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
|
||||||
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
|
||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
|
||||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
|
||||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
|
||||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
|
||||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
|
||||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
|
||||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
|
||||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
Before Width: | Height: | Size: 514 KiB |
|
Before Width: | Height: | Size: 252 KiB |
|
Before Width: | Height: | Size: 795 KiB |
|
Before Width: | Height: | Size: 356 KiB |
|
Before Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 448 KiB |
|
Before Width: | Height: | Size: 460 KiB |
|
Before Width: | Height: | Size: 696 KiB |
|
Before Width: | Height: | Size: 456 KiB |
@ -1,860 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
"github.com/oschwald/geoip2-golang"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
📦 AdGuard Exporter for Prometheus
|
|
||||||
----------------------------------
|
|
||||||
Author : @znand-dev
|
|
||||||
License : MIT
|
|
||||||
Repo : https://github.com/znand-dev/adguardexporter
|
|
||||||
|
|
||||||
This Go application fetches stats from AdGuard Home via API endpoints
|
|
||||||
and exposes them as Prometheus metrics at `/metrics`.
|
|
||||||
|
|
||||||
Required ENV variables:
|
|
||||||
- ADGUARD_HOST : AdGuard Home base URL (e.g. http://192.168.1.1:3000)
|
|
||||||
- ADGUARD_USER : API username (your adguard user)
|
|
||||||
- ADGUARD_PASS : API password (your adguard pass)
|
|
||||||
- EXPORTER_PORT : Port to expose metrics (default: 9617)
|
|
||||||
- SCRAPE_INTERVAL : Interval (in seconds) to fetch new stats (default: 15)
|
|
||||||
- LOG_LEVEL : Logging level (options: DEBUG, INFO, WARN, ERROR — default: INFO)
|
|
||||||
- GEOIP_DB : GeoLite2-City.mmdb
|
|
||||||
*/
|
|
||||||
|
|
||||||
var logLevelMap = map[string]int{"ERROR": 1, "WARN": 2, "INFO": 3, "DEBUG": 4}
|
|
||||||
var currentLogLevel = 3
|
|
||||||
|
|
||||||
func initLogger() {
|
|
||||||
level := os.Getenv("LOG_LEVEL")
|
|
||||||
if level == "" {
|
|
||||||
level = "INFO"
|
|
||||||
}
|
|
||||||
if val, ok := logLevelMap[level]; ok {
|
|
||||||
currentLogLevel = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func logX(level string, format string, args ...interface{}) {
|
|
||||||
if logLevelMap[level] <= currentLogLevel {
|
|
||||||
log.Printf("[%s] %s", level, fmt.Sprintf(format, args...))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//function hash query
|
|
||||||
func buildQueryKey(client, domain, upstream, reason, elapsed string) string {
|
|
||||||
return client + "|" + domain + "|" + upstream + "|" + reason + "|" + elapsed
|
|
||||||
}
|
|
||||||
|
|
||||||
type GeoCache struct {
|
|
||||||
Country string
|
|
||||||
Lat float64
|
|
||||||
Lon float64
|
|
||||||
TS int64
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
geoDB *geoip2.Reader
|
|
||||||
geoCache = map[string]GeoCache{}
|
|
||||||
geoMutex sync.RWMutex
|
|
||||||
// cache query hash
|
|
||||||
querySeen = map[string]int64{}
|
|
||||||
queryMutex sync.Mutex
|
|
||||||
queryTTL = int64(300) // 5 menit
|
|
||||||
geoTTL = int64(86400) // 24 jam
|
|
||||||
dedupHits int64
|
|
||||||
)
|
|
||||||
|
|
||||||
type AdGuardStats struct {
|
|
||||||
NumDNSQueries float64 `json:"num_dns_queries"`
|
|
||||||
NumBlockedFiltering float64 `json:"num_blocked_filtering"`
|
|
||||||
NumReplacedParental float64 `json:"num_replaced_parental"`
|
|
||||||
AvgProcessingTime float64 `json:"avg_processing_time"`
|
|
||||||
TopQueriedDomains []map[string]float64 `json:"top_queried_domains"`
|
|
||||||
TopBlockedDomains []map[string]float64 `json:"top_blocked_domains"`
|
|
||||||
TopClients []map[string]float64 `json:"top_clients"`
|
|
||||||
TopUpstream []map[string]float64 `json:"top_upstreams_responses"`
|
|
||||||
TopUpstreamTime []map[string]float64 `json:"top_upstreams_avg_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdGuardStatus struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
Language string `json:"language"`
|
|
||||||
DNSAddresses []string `json:"dns_addresses"`
|
|
||||||
DNSPort int `json:"dns_port"`
|
|
||||||
HTTPPort int `json:"http_port"`
|
|
||||||
ProtectionDisabledDuration int `json:"protection_disabled_duration"`
|
|
||||||
ProtectionEnabled bool `json:"protection_enabled"`
|
|
||||||
DHCPAvailable bool `json:"dhcp_available"`
|
|
||||||
Running bool `json:"running"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdGuardQueryLog struct {
|
|
||||||
Data []struct {
|
|
||||||
Question struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
} `json:"question"`
|
|
||||||
Answer []interface{} `json:"answer"`
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
Client string `json:"client"`
|
|
||||||
Elapsed string `json:"elapsedMs"`
|
|
||||||
Upstream string `json:"upstream"`
|
|
||||||
|
|
||||||
ClientInfo struct {
|
|
||||||
Whois struct {
|
|
||||||
Country string `json:"country"`
|
|
||||||
OrgName string `json:"orgname"`
|
|
||||||
} `json:"whois"`
|
|
||||||
} `json:"client_info"`
|
|
||||||
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
|
|
||||||
dnsQueries = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_dns_queries_total", Help: "Total DNS queries received",
|
|
||||||
})
|
|
||||||
|
|
||||||
blockedFiltering = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_blocked_filtering_total", Help: "Total DNS queries blocked",
|
|
||||||
})
|
|
||||||
|
|
||||||
replacedParental = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_replaced_parental", Help: "Total parental replaced queries",
|
|
||||||
})
|
|
||||||
|
|
||||||
avgProcessingTime = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_avg_processing_time", Help: "Avg DNS processing time",
|
|
||||||
})
|
|
||||||
|
|
||||||
statusProtectionEnabled = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_protection_enabled", Help: "Protection enabled (1/0)",
|
|
||||||
})
|
|
||||||
|
|
||||||
statusRunning = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_running", Help: "AdGuard running",
|
|
||||||
})
|
|
||||||
|
|
||||||
statusDHCPAvailable = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_dhcp_available", Help: "DHCP available",
|
|
||||||
})
|
|
||||||
|
|
||||||
statusDisabledDuration = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_protection_disabled_duration_seconds",
|
|
||||||
Help: "Time since protection disabled",
|
|
||||||
})
|
|
||||||
|
|
||||||
versionInfo = prometheus.NewGaugeVec(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_version_info",
|
|
||||||
Help: "AdGuard version",
|
|
||||||
},
|
|
||||||
[]string{"version"},
|
|
||||||
)
|
|
||||||
|
|
||||||
topQueriedDomains = prometheus.NewGaugeVec(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_top_queried_domain_total",
|
|
||||||
Help: "Top queried domains",
|
|
||||||
},
|
|
||||||
[]string{"domain"},
|
|
||||||
)
|
|
||||||
|
|
||||||
topBlockedDomains = prometheus.NewGaugeVec(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_top_blocked_domain_total",
|
|
||||||
Help: "Top blocked domains",
|
|
||||||
},
|
|
||||||
[]string{"domain"},
|
|
||||||
)
|
|
||||||
|
|
||||||
topClients = prometheus.NewGaugeVec(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_top_client_total",
|
|
||||||
Help: "Top client IPs",
|
|
||||||
},
|
|
||||||
[]string{"client"},
|
|
||||||
)
|
|
||||||
|
|
||||||
topUpstreams = prometheus.NewGaugeVec(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_top_upstream_total",
|
|
||||||
Help: "Top upstream servers",
|
|
||||||
},
|
|
||||||
[]string{"upstream"},
|
|
||||||
)
|
|
||||||
|
|
||||||
topUpstreamTime = prometheus.NewGaugeVec(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_upstream_avg_response_time_seconds",
|
|
||||||
Help: "Avg response time per upstream",
|
|
||||||
},
|
|
||||||
[]string{"upstream"},
|
|
||||||
)
|
|
||||||
|
|
||||||
queryCountByReason = prometheus.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "adguard_query_reason_total",
|
|
||||||
Help: "Queries by reason",
|
|
||||||
},
|
|
||||||
[]string{"reason"},
|
|
||||||
)
|
|
||||||
|
|
||||||
queryCountByType = prometheus.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "adguard_query_type_total",
|
|
||||||
Help: "Queries by DNS type",
|
|
||||||
},
|
|
||||||
[]string{"type"},
|
|
||||||
)
|
|
||||||
|
|
||||||
queryHistogramByClient = prometheus.NewHistogramVec(
|
|
||||||
prometheus.HistogramOpts{
|
|
||||||
Name: "adguard_query_elapsed_ms",
|
|
||||||
Help: "Query duration by client",
|
|
||||||
Buckets: prometheus.LinearBuckets(1, 5, 10),
|
|
||||||
},
|
|
||||||
[]string{"client"},
|
|
||||||
)
|
|
||||||
|
|
||||||
queryCountByUpstream = prometheus.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "adguard_query_upstream_total",
|
|
||||||
Help: "Queries per upstream",
|
|
||||||
},
|
|
||||||
[]string{"upstream"},
|
|
||||||
)
|
|
||||||
|
|
||||||
queryCountByDomain = prometheus.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "adguard_query_domain_total",
|
|
||||||
Help: "Queries per domain",
|
|
||||||
},
|
|
||||||
[]string{"domain"},
|
|
||||||
)
|
|
||||||
|
|
||||||
queryCountClientReason = prometheus.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "adguard_query_client_reason_total",
|
|
||||||
Help: "Queries per client per reason",
|
|
||||||
},
|
|
||||||
[]string{"client", "reason"},
|
|
||||||
)
|
|
||||||
|
|
||||||
clientGeoQueries = prometheus.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "adguard_client_geo_queries",
|
|
||||||
Help: "Queries per client with geographic info",
|
|
||||||
},
|
|
||||||
[]string{"client", "country", "lat", "lon"},
|
|
||||||
)
|
|
||||||
|
|
||||||
blockedGeoQueries = prometheus.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "adguard_blocked_geo_queries",
|
|
||||||
Help: "Blocked DNS queries per client with geographic info",
|
|
||||||
},
|
|
||||||
[]string{"client", "country", "lat", "lon"},
|
|
||||||
)
|
|
||||||
|
|
||||||
upstreamLatencyHistogram = prometheus.NewHistogramVec(
|
|
||||||
prometheus.HistogramOpts{
|
|
||||||
Name: "adguard_upstream_latency_seconds",
|
|
||||||
Help: "Latency distribution per upstream DNS server",
|
|
||||||
Buckets: prometheus.ExponentialBuckets(
|
|
||||||
0.001, // 1ms
|
|
||||||
2,
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
[]string{"upstream"},
|
|
||||||
)
|
|
||||||
|
|
||||||
exporterUp = prometheus.NewGauge(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_exporter_up",
|
|
||||||
Help: "Exporter scrape success",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
exporterScrapeDuration = prometheus.NewGauge(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_exporter_scrape_duration_seconds",
|
|
||||||
Help: "Exporter scrape duration",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
exporterErrors = prometheus.NewCounter(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "adguard_exporter_scrape_errors_total",
|
|
||||||
Help: "Total exporter errors",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
exporterQueryCacheSize = prometheus.NewGauge(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_exporter_query_cache_size",
|
|
||||||
Help: "Current number of entries in query deduplication cache",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
exporterGeoCacheSize = prometheus.NewGauge(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "adguard_exporter_geo_cache_size",
|
|
||||||
Help: "Current number of cached GeoIP entries",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
exporterDedupHits = prometheus.NewCounter(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "adguard_exporter_dedup_hits_total",
|
|
||||||
Help: "Total number of duplicate queries skipped by deduplication",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
queryCountByISP = prometheus.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "adguard_query_isp_total",
|
|
||||||
Help: "DNS queries grouped by ISP organization",
|
|
||||||
},
|
|
||||||
[]string{"isp", "country"},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
_ = godotenv.Load()
|
|
||||||
initLogger()
|
|
||||||
|
|
||||||
prometheus.MustRegister(
|
|
||||||
dnsQueries,
|
|
||||||
blockedFiltering,
|
|
||||||
replacedParental,
|
|
||||||
avgProcessingTime,
|
|
||||||
statusProtectionEnabled,
|
|
||||||
statusRunning,
|
|
||||||
statusDHCPAvailable,
|
|
||||||
statusDisabledDuration,
|
|
||||||
versionInfo,
|
|
||||||
topQueriedDomains,
|
|
||||||
topBlockedDomains,
|
|
||||||
topClients,
|
|
||||||
topUpstreams,
|
|
||||||
topUpstreamTime,
|
|
||||||
queryCountByReason,
|
|
||||||
queryCountByType,
|
|
||||||
queryHistogramByClient,
|
|
||||||
queryCountByUpstream,
|
|
||||||
queryCountByDomain,
|
|
||||||
queryCountClientReason,
|
|
||||||
clientGeoQueries,
|
|
||||||
blockedGeoQueries,
|
|
||||||
exporterUp,
|
|
||||||
exporterScrapeDuration,
|
|
||||||
exporterErrors,
|
|
||||||
upstreamLatencyHistogram,
|
|
||||||
exporterQueryCacheSize,
|
|
||||||
exporterGeoCacheSize,
|
|
||||||
exporterDedupHits,
|
|
||||||
queryCountByISP,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveGeo(ipStr string) (GeoCache, bool) {
|
|
||||||
if geoDB == nil {
|
|
||||||
return GeoCache{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
ip := net.ParseIP(ipStr)
|
|
||||||
if ip == nil || ip.IsPrivate() || ip.IsLoopback() {
|
|
||||||
return GeoCache{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now().Unix()
|
|
||||||
|
|
||||||
geoMutex.RLock()
|
|
||||||
if val, ok := geoCache[ipStr]; ok {
|
|
||||||
|
|
||||||
// cache valid
|
|
||||||
if now-val.TS < geoTTL {
|
|
||||||
geoMutex.RUnlock()
|
|
||||||
return val, true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
geoMutex.RUnlock()
|
|
||||||
|
|
||||||
record, err := geoDB.City(ip)
|
|
||||||
if err != nil {
|
|
||||||
return GeoCache{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
cache := GeoCache{
|
|
||||||
Country: record.Country.IsoCode,
|
|
||||||
Lat: record.Location.Latitude,
|
|
||||||
Lon: record.Location.Longitude,
|
|
||||||
TS: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
geoMutex.Lock()
|
|
||||||
geoCache[ipStr] = cache
|
|
||||||
geoMutex.Unlock()
|
|
||||||
|
|
||||||
return cache, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchQueryLog() (*AdGuardQueryLog, error) {
|
|
||||||
|
|
||||||
host := os.Getenv("ADGUARD_HOST")
|
|
||||||
user := os.Getenv("ADGUARD_USER")
|
|
||||||
pass := os.Getenv("ADGUARD_PASS")
|
|
||||||
|
|
||||||
url := host + "/control/querylog"
|
|
||||||
|
|
||||||
req, _ := http.NewRequest("GET", url, nil)
|
|
||||||
req.SetBasicAuth(user, pass)
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: 10 * time.Second}
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err := resp.Body.Close(); err != nil {
|
|
||||||
logX("WARN", "failed to close response body: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var logData AdGuardQueryLog
|
|
||||||
|
|
||||||
err = json.Unmarshal(body, &logData)
|
|
||||||
|
|
||||||
return &logData, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchStats() (*AdGuardStats, error) {
|
|
||||||
|
|
||||||
host := os.Getenv("ADGUARD_HOST")
|
|
||||||
user := os.Getenv("ADGUARD_USER")
|
|
||||||
pass := os.Getenv("ADGUARD_PASS")
|
|
||||||
|
|
||||||
url := host + "/control/stats"
|
|
||||||
|
|
||||||
req, _ := http.NewRequest("GET", url, nil)
|
|
||||||
req.SetBasicAuth(user, pass)
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: 10 * time.Second}
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err := resp.Body.Close(); err != nil {
|
|
||||||
logX("WARN", "failed to close response body: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var stats AdGuardStats
|
|
||||||
|
|
||||||
err = json.Unmarshal(body, &stats)
|
|
||||||
|
|
||||||
return &stats, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchStatus() (*AdGuardStatus, error) {
|
|
||||||
|
|
||||||
host := os.Getenv("ADGUARD_HOST")
|
|
||||||
user := os.Getenv("ADGUARD_USER")
|
|
||||||
pass := os.Getenv("ADGUARD_PASS")
|
|
||||||
|
|
||||||
url := host + "/control/status"
|
|
||||||
|
|
||||||
req, _ := http.NewRequest("GET", url, nil)
|
|
||||||
req.SetBasicAuth(user, pass)
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: 10 * time.Second}
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err := resp.Body.Close(); err != nil {
|
|
||||||
logX("WARN", "failed to close response body: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var status AdGuardStatus
|
|
||||||
|
|
||||||
err = json.Unmarshal(body, &status)
|
|
||||||
|
|
||||||
return &status, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateStatsMetrics() {
|
|
||||||
|
|
||||||
stats, err := fetchStats()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logX("ERROR", "Failed fetch stats: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dnsQueries.Set(stats.NumDNSQueries)
|
|
||||||
blockedFiltering.Set(stats.NumBlockedFiltering)
|
|
||||||
replacedParental.Set(stats.NumReplacedParental)
|
|
||||||
avgProcessingTime.Set(stats.AvgProcessingTime)
|
|
||||||
|
|
||||||
// reset gauge vectors
|
|
||||||
topQueriedDomains.Reset()
|
|
||||||
topBlockedDomains.Reset()
|
|
||||||
topClients.Reset()
|
|
||||||
topUpstreams.Reset()
|
|
||||||
topUpstreamTime.Reset()
|
|
||||||
|
|
||||||
// top queried domains
|
|
||||||
for _, d := range stats.TopQueriedDomains {
|
|
||||||
for domain, count := range d {
|
|
||||||
topQueriedDomains.WithLabelValues(domain).Set(count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// top blocked domains
|
|
||||||
for _, d := range stats.TopBlockedDomains {
|
|
||||||
for domain, count := range d {
|
|
||||||
topBlockedDomains.WithLabelValues(domain).Set(count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// top clients
|
|
||||||
for _, c := range stats.TopClients {
|
|
||||||
for client, count := range c {
|
|
||||||
topClients.WithLabelValues(client).Set(count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// upstream responses
|
|
||||||
for _, u := range stats.TopUpstream {
|
|
||||||
for upstream, count := range u {
|
|
||||||
topUpstreams.WithLabelValues(upstream).Set(count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// upstream avg response time
|
|
||||||
for _, u := range stats.TopUpstreamTime {
|
|
||||||
for upstream, time := range u {
|
|
||||||
topUpstreamTime.WithLabelValues(upstream).Set(time)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logX(
|
|
||||||
"DEBUG",
|
|
||||||
"Fetched stats: queries=%.0f blocked=%.0f replaced=%.0f avgTime=%.2fms topDomains=%d",
|
|
||||||
stats.NumDNSQueries,
|
|
||||||
stats.NumBlockedFiltering,
|
|
||||||
stats.NumReplacedParental,
|
|
||||||
stats.AvgProcessingTime,
|
|
||||||
len(stats.TopQueriedDomains),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateStatusMetrics() {
|
|
||||||
|
|
||||||
status, err := fetchStatus()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logX("ERROR", "Failed fetch status: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if status.Running {
|
|
||||||
statusRunning.Set(1)
|
|
||||||
} else {
|
|
||||||
statusRunning.Set(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if status.ProtectionEnabled {
|
|
||||||
statusProtectionEnabled.Set(1)
|
|
||||||
} else {
|
|
||||||
statusProtectionEnabled.Set(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if status.DHCPAvailable {
|
|
||||||
statusDHCPAvailable.Set(1)
|
|
||||||
} else {
|
|
||||||
statusDHCPAvailable.Set(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
statusDisabledDuration.Set(float64(status.ProtectionDisabledDuration))
|
|
||||||
|
|
||||||
versionInfo.WithLabelValues(status.Version).Set(1)
|
|
||||||
|
|
||||||
logX(
|
|
||||||
"DEBUG",
|
|
||||||
"Fetched status: running=%v protection=%v DHCP=%v version=%s",
|
|
||||||
status.Running,
|
|
||||||
status.ProtectionEnabled,
|
|
||||||
status.DHCPAvailable,
|
|
||||||
status.Version,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateQueryLogMetrics() {
|
|
||||||
|
|
||||||
scanned := 0
|
|
||||||
processed := 0
|
|
||||||
skipped := 0
|
|
||||||
geoResolved := 0
|
|
||||||
privateClients := 0
|
|
||||||
publicClients := 0
|
|
||||||
|
|
||||||
logData, err := fetchQueryLog()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logX("ERROR", "Failed querylog: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, q := range logData.Data {
|
|
||||||
|
|
||||||
scanned++
|
|
||||||
|
|
||||||
ip := net.ParseIP(q.Client)
|
|
||||||
|
|
||||||
if ip != nil {
|
|
||||||
if ip.IsPrivate() || ip.IsLoopback() {
|
|
||||||
privateClients++
|
|
||||||
} else {
|
|
||||||
publicClients++
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// dedup logic
|
|
||||||
key := buildQueryKey(
|
|
||||||
q.Client,
|
|
||||||
q.Question.Name,
|
|
||||||
q.Upstream,
|
|
||||||
q.Reason,
|
|
||||||
q.Elapsed,
|
|
||||||
)
|
|
||||||
|
|
||||||
now := time.Now().Unix()
|
|
||||||
|
|
||||||
queryMutex.Lock()
|
|
||||||
|
|
||||||
if ts, exists := querySeen[key]; exists {
|
|
||||||
if now-ts < queryTTL {
|
|
||||||
queryMutex.Unlock()
|
|
||||||
skipped++
|
|
||||||
exporterDedupHits.Inc()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
querySeen[key] = now
|
|
||||||
queryMutex.Unlock()
|
|
||||||
|
|
||||||
processed++
|
|
||||||
|
|
||||||
queryCountByReason.WithLabelValues(q.Reason).Inc()
|
|
||||||
queryCountByType.WithLabelValues(q.Question.Type).Inc()
|
|
||||||
|
|
||||||
elapsedMs, err := strconv.ParseFloat(q.Elapsed, 64)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
|
|
||||||
queryHistogramByClient.WithLabelValues(q.Client).Observe(elapsedMs)
|
|
||||||
|
|
||||||
if q.Upstream != "" {
|
|
||||||
upstreamLatencyHistogram.WithLabelValues(q.Upstream).Observe(elapsedMs / 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
queryCountByUpstream.WithLabelValues(q.Upstream).Inc()
|
|
||||||
queryCountByDomain.WithLabelValues(q.Question.Name).Inc()
|
|
||||||
queryCountClientReason.WithLabelValues(q.Client, q.Reason).Inc()
|
|
||||||
|
|
||||||
geo, ok := resolveGeo(q.Client)
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
|
|
||||||
geoResolved++
|
|
||||||
|
|
||||||
clientGeoQueries.WithLabelValues(
|
|
||||||
q.Client,
|
|
||||||
geo.Country,
|
|
||||||
fmt.Sprintf("%f", geo.Lat),
|
|
||||||
fmt.Sprintf("%f", geo.Lon),
|
|
||||||
).Inc()
|
|
||||||
|
|
||||||
if q.Reason == "FilteredBlackList" ||
|
|
||||||
q.Reason == "FilteredSafeBrowsing" ||
|
|
||||||
q.Reason == "FilteredParental" {
|
|
||||||
|
|
||||||
blockedGeoQueries.WithLabelValues(
|
|
||||||
q.Client,
|
|
||||||
geo.Country,
|
|
||||||
fmt.Sprintf("%f", geo.Lat),
|
|
||||||
fmt.Sprintf("%f", geo.Lon),
|
|
||||||
).Inc()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isp := q.ClientInfo.Whois.OrgName
|
|
||||||
country := q.ClientInfo.Whois.Country
|
|
||||||
|
|
||||||
if isp != "" {
|
|
||||||
queryCountByISP.WithLabelValues(isp, country).Inc()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
logX("DEBUG",
|
|
||||||
"Querylog: scanned=%d new=%d skipped=%d geoip=%d dedupHits=%d",
|
|
||||||
scanned,
|
|
||||||
processed,
|
|
||||||
skipped,
|
|
||||||
geoResolved,
|
|
||||||
dedupHits,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
//cleanup memory
|
|
||||||
func cleanupQueryCache() {
|
|
||||||
|
|
||||||
now := time.Now().Unix()
|
|
||||||
|
|
||||||
queryMutex.Lock()
|
|
||||||
|
|
||||||
for k, v := range querySeen {
|
|
||||||
|
|
||||||
if now-v > queryTTL {
|
|
||||||
delete(querySeen, k)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
queryMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
|
|
||||||
dbPath := os.Getenv("GEOIP_DB")
|
|
||||||
|
|
||||||
if dbPath == "" {
|
|
||||||
dbPath = "GeoLite2-City.mmdb"
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := geoip2.Open(dbPath)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logX("WARN", "GeoIP DB not found (%v) — geo metrics disabled", err)
|
|
||||||
} else {
|
|
||||||
geoDB = db
|
|
||||||
logX("INFO", "GeoIP database loaded")
|
|
||||||
}
|
|
||||||
|
|
||||||
scrapeIntervalStr := os.Getenv("SCRAPE_INTERVAL")
|
|
||||||
port := os.Getenv("EXPORTER_PORT")
|
|
||||||
|
|
||||||
if port == "" {
|
|
||||||
port = "9617"
|
|
||||||
}
|
|
||||||
|
|
||||||
interval, err := strconv.Atoi(scrapeIntervalStr)
|
|
||||||
|
|
||||||
if err != nil || interval < 1 {
|
|
||||||
interval = 15
|
|
||||||
}
|
|
||||||
|
|
||||||
logX("INFO", "Scrape interval set to %ds", interval)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
|
|
||||||
for {
|
|
||||||
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
updateStatsMetrics()
|
|
||||||
updateStatusMetrics()
|
|
||||||
updateQueryLogMetrics()
|
|
||||||
|
|
||||||
cleanupQueryCache()
|
|
||||||
|
|
||||||
duration := time.Since(start).Seconds()
|
|
||||||
|
|
||||||
exporterScrapeDuration.Set(duration)
|
|
||||||
exporterUp.Set(1)
|
|
||||||
|
|
||||||
exporterQueryCacheSize.Set(float64(len(querySeen)))
|
|
||||||
exporterGeoCacheSize.Set(float64(len(geoCache)))
|
|
||||||
|
|
||||||
logX(
|
|
||||||
"DEBUG",
|
|
||||||
"Exporter caches: query_cache_entries=%d geo_cache_entries=%d dedup_hits_total=%d",
|
|
||||||
len(querySeen),
|
|
||||||
len(geoCache),
|
|
||||||
dedupHits,
|
|
||||||
)
|
|
||||||
|
|
||||||
logX("DEBUG", "Scrape finished in %.3fs", duration)
|
|
||||||
|
|
||||||
time.Sleep(time.Duration(interval) * time.Second)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}()
|
|
||||||
|
|
||||||
http.Handle("/metrics", promhttp.Handler())
|
|
||||||
|
|
||||||
logX("INFO", "Starting exporter at :%s ..", port)
|
|
||||||
|
|
||||||
err = http.ListenAndServe(":"+port, nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logX("ERROR", "Server failed: %v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||