refactor: config/apps 目录重组、文档重构、架构图收窄

- 中央:config/(prometheus,alertmanager,grafana)、apps/(tile-cache,topology-editor)
- 边缘:config/(vmagent,blackbox,targets)、apps/(onvif-exporter)
- env: TRAEFIK_PROVIDER、prometheus/env.example 详细说明
- 文档:README/doc 重构,EDGE_CACHE 合并到 EDGE_AGENT_CONFIG
- targets.csv 更新流程说明,ARCHITECTURE 图收窄

Made-with: Cursor
This commit is contained in:
2026-02-28 22:05:43 -05:00
parent 650e5145f1
commit ab1515dffb
48 changed files with 2071 additions and 509 deletions

View File

@@ -0,0 +1,12 @@
# 多阶段构建:在镜像内编译,无需本机安装 Go
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY apps/onvif-exporter/go.mod ./
COPY apps/onvif-exporter/main.go ./
RUN go mod tidy && CGO_ENABLED=0 GOOS=linux go build -o /onvif-exporter .
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY --from=builder /onvif-exporter /onvif-exporter
EXPOSE 9600
ENTRYPOINT ["/onvif-exporter"]

View File

@@ -0,0 +1,37 @@
# ONVIF Exporter自建
本目录为自建的 ONVIF 探测容器,供边缘节点可选使用。通过 ONVIF `GetDeviceInformation` 探测设备是否在线,并暴露 Prometheus 指标。
## 指标
- `onvif_device_up`1=可达0=不可达
- 标签:`instance`, `location`, `model`, `device_type`, `lat`, `lon`
- `onvif_probe_duration_seconds`:探测耗时(秒)
- 标签同上
## 配置
-**config/target-onvif.json** 读取设备列表(与 `targets.csv` 中 onvif 行一致,由 `config/update-configs.sh` 生成)。
- 支持字段:`device_type`, `model`, `location`, `username`, `password`, `onvif_port`, `lat`, `lon`
- 环境变量:`TARGETS_FILE`(默认 `/config/targets.json`)、`EXPORTER_PORT`(默认 9600
## 构建与运行
在边缘节点目录下启用 ONVIF 并构建、启动:
```bash
cd edge-agent
docker compose --profile onvif up -d --build
```
或仅构建镜像:
```bash
docker build -t onvif-exporter:local ./onvif-exporter
```
## 依赖
- Go 1.21仅构建时需要Dockerfile 内已包含)
- [github.com/use-go/onvif](https://github.com/use-go/onvif)ONVIF 协议)
- [prometheus/client_golang](https://github.com/prometheus/client_golang)(指标暴露)

View File

@@ -0,0 +1,24 @@
module github.com/distributed-prometheus/onvif-exporter
go 1.21
require (
github.com/prometheus/client_golang v1.19.0
github.com/use-go/onvif v0.0.9
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/elgs/gostrgen v0.0.0-20161222160715-9d61ae07eeae // indirect
github.com/gofrs/uuid v3.2.0+incompatible // indirect
github.com/juju/errors v0.0.0-20220331221717-b38fca44723b // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.50.0 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
github.com/rs/zerolog v1.26.1 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.17.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
)

View File

@@ -0,0 +1,126 @@
package main
import (
"encoding/json"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/use-go/onvif"
"github.com/use-go/onvif/device"
)
const (
defaultTargetsFile = "/config/targets.json"
defaultPort = "9600"
)
type targetGroup struct {
Targets []string `json:"targets"`
Labels map[string]string `json:"labels"`
}
func main() {
targetsFile := os.Getenv("TARGETS_FILE")
if targetsFile == "" {
targetsFile = defaultTargetsFile
}
port := os.Getenv("EXPORTER_PORT")
if port == "" {
port = defaultPort
}
up := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "onvif_device_up",
Help: "1 if ONVIF device is reachable, 0 otherwise",
}, []string{"instance", "location", "model", "device_type", "lat", "lon"})
duration := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "onvif_probe_duration_seconds",
Help: "ONVIF probe duration in seconds",
}, []string{"instance", "location", "model", "device_type", "lat", "lon"})
reg := prometheus.NewRegistry()
reg.MustRegister(up, duration)
go func() {
ticker := time.NewTicker(60 * time.Second)
defer ticker.Stop()
for ; true; <-ticker.C {
probe(targetsFile, up, duration)
}
}()
// 启动时立即探测一次
probe(targetsFile, up, duration)
http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{EnableOpenMetrics: true}))
http.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })
log.Printf("ONVIF exporter listening on :%s", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)
}
}
func probe(targetsFile string, up, duration *prometheus.GaugeVec) {
data, err := os.ReadFile(targetsFile)
if err != nil {
log.Printf("read targets file: %v", err)
return
}
var groups []targetGroup
if err := json.Unmarshal(data, &groups); err != nil {
log.Printf("parse targets: %v", err)
return
}
for _, g := range groups {
if len(g.Targets) == 0 {
continue
}
ip := strings.TrimSpace(g.Targets[0])
port := "80"
if p, ok := g.Labels["onvif_port"]; ok && p != "" {
port = strings.TrimSpace(p)
}
user := strings.TrimSpace(g.Labels["username"])
pass := strings.TrimSpace(g.Labels["password"])
location := strings.TrimSpace(g.Labels["location"])
lat := strings.TrimSpace(g.Labels["lat"])
lon := strings.TrimSpace(g.Labels["lon"])
model := strings.TrimSpace(g.Labels["model"])
deviceType := strings.TrimSpace(g.Labels["device_type"])
instance := ip + ":" + port
labels := prometheus.Labels{
"instance": instance,
"location": location,
"model": model,
"device_type": deviceType,
"lat": lat,
"lon": lon,
}
start := time.Now()
err := probeONVIF(ip, port, user, pass)
elapsed := time.Since(start).Seconds()
duration.With(labels).Set(elapsed)
if err != nil {
log.Printf("onvif probe %s: %v", instance, err)
up.With(labels).Set(0)
} else {
up.With(labels).Set(1)
}
}
}
func probeONVIF(ip, port, username, password string) error {
xaddr := ip + ":" + port
params := onvif.DeviceParams{Xaddr: xaddr, Username: username, Password: password}
dev, err := onvif.NewDevice(params)
if err != nil {
return err
}
_, err = dev.CallMethod(device.GetDeviceInformation{})
return err
}