From 0f49b1e04e137b9705094c84e5cc62edff699a80 Mon Sep 17 00:00:00 2001 From: wangqing Date: Fri, 10 Oct 2025 11:08:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: wangqing --- .gitignore | 48 ++++++++ go.mod | 21 ++++ go.sum | 48 ++++++++ libvirt_collector.go | 278 +++++++++++++++++++++++++++++++++++++++++++ main.go | 69 +++++++++++ 5 files changed, 464 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 go.sum create mode 100644 libvirt_collector.go create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..12de8b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# Binary output +uos-libvirtd-exporter +uos-libvirtd-exporter-* + +# IDE specific files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS specific files +.DS_Store +Thumbs.db + +# Build artifacts +dist/ +build/ + +# Environment files +.env +.env.local + +# Log files +*.log + +# Temporary files +tmp/ +temp/ \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0185316 --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module gitee.com/openeuler/uos-libvirtd-exporter + +go 1.25.1 + +require ( + github.com/prometheus/client_golang v1.23.2 + libvirt.org/go/libvirt v1.11006.0 +) + +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/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.35.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fa466be --- /dev/null +++ b/go.sum @@ -0,0 +1,48 @@ +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/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/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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +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= +libvirt.org/go/libvirt v1.11006.0 h1:xzF87ptj/7cp1h4T62w1ZMBVY8m0mQukSCstMgeiVLs= +libvirt.org/go/libvirt v1.11006.0/go.mod h1:1WiFE8EjZfq+FCVog+rvr1yatKbKZ9FaFMZgEqxEJqQ= diff --git a/libvirt_collector.go b/libvirt_collector.go new file mode 100644 index 0000000..aec76b5 --- /dev/null +++ b/libvirt_collector.go @@ -0,0 +1,278 @@ +package main + +import ( + "log" + "sync" + "time" + + "libvirt.org/go/libvirt" + "github.com/prometheus/client_golang/prometheus" +) + +// LibvirtCollector implements the prometheus.Collector interface +type LibvirtCollector struct { + uri string + conn *libvirt.Connect + mutex sync.RWMutex + + // VM status metrics + vmStatus *prometheus.Desc + + // CPU metrics + vmCPUTime *prometheus.Desc + + // Memory metrics + vmMemoryCurrent *prometheus.Desc + vmMemoryMax *prometheus.Desc + + // Disk I/O metrics + vmDiskReadBytes *prometheus.Desc + vmDiskWriteBytes *prometheus.Desc + vmDiskReadOps *prometheus.Desc + vmDiskWriteOps *prometheus.Desc + + // Network I/O metrics + vmNetworkRxBytes *prometheus.Desc + vmNetworkTxBytes *prometheus.Desc + vmNetworkRxPkts *prometheus.Desc + vmNetworkTxPkts *prometheus.Desc + + // Uptime metrics + vmUptime *prometheus.Desc +} + +// NewLibvirtCollector creates a new LibvirtCollector +func NewLibvirtCollector(uri string) (*LibvirtCollector, error) { + conn, err := libvirt.NewConnect(uri) + if err != nil { + return nil, err + } + + return &LibvirtCollector{ + uri: uri, + conn: conn, + + vmStatus: prometheus.NewDesc( + "libvirt_vm_status", + "Status of the virtual machine (1=running, 0=other)", + []string{"domain", "uuid"}, + nil, + ), + + vmCPUTime: prometheus.NewDesc( + "libvirt_vm_cpu_time_seconds_total", + "Total CPU time used by the virtual machine in seconds", + []string{"domain", "uuid"}, + nil, + ), + + vmMemoryCurrent: prometheus.NewDesc( + "libvirt_vm_memory_current_bytes", + "Current memory usage of the virtual machine in bytes", + []string{"domain", "uuid"}, + nil, + ), + + vmMemoryMax: prometheus.NewDesc( + "libvirt_vm_memory_max_bytes", + "Maximum memory allowed for the virtual machine in bytes", + []string{"domain", "uuid"}, + nil, + ), + + vmDiskReadBytes: prometheus.NewDesc( + "libvirt_vm_disk_read_bytes_total", + "Total bytes read from disk by the virtual machine", + []string{"domain", "uuid", "device"}, + nil, + ), + + vmDiskWriteBytes: prometheus.NewDesc( + "libvirt_vm_disk_write_bytes_total", + "Total bytes written to disk by the virtual machine", + []string{"domain", "uuid", "device"}, + nil, + ), + + vmDiskReadOps: prometheus.NewDesc( + "libvirt_vm_disk_read_ops_total", + "Total disk read operations by the virtual machine", + []string{"domain", "uuid", "device"}, + nil, + ), + + vmDiskWriteOps: prometheus.NewDesc( + "libvirt_vm_disk_write_ops_total", + "Total disk write operations by the virtual machine", + []string{"domain", "uuid", "device"}, + nil, + ), + + vmNetworkRxBytes: prometheus.NewDesc( + "libvirt_vm_network_rx_bytes_total", + "Total network bytes received by the virtual machine", + []string{"domain", "uuid", "interface"}, + nil, + ), + + vmNetworkTxBytes: prometheus.NewDesc( + "libvirt_vm_network_tx_bytes_total", + "Total network bytes transmitted by the virtual machine", + []string{"domain", "uuid", "interface"}, + nil, + ), + + vmNetworkRxPkts: prometheus.NewDesc( + "libvirt_vm_network_rx_packets_total", + "Total network packets received by the virtual machine", + []string{"domain", "uuid", "interface"}, + nil, + ), + + vmNetworkTxPkts: prometheus.NewDesc( + "libvirt_vm_network_tx_packets_total", + "Total network packets transmitted by the virtual machine", + []string{"domain", "uuid", "interface"}, + nil, + ), + + vmUptime: prometheus.NewDesc( + "libvirt_vm_uptime_seconds", + "Virtual machine uptime in seconds", + []string{"domain", "uuid"}, + nil, + ), + }, nil +} + +// Describe implements the prometheus.Collector interface +func (c *LibvirtCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- c.vmStatus + ch <- c.vmCPUTime + ch <- c.vmMemoryCurrent + ch <- c.vmMemoryMax + ch <- c.vmDiskReadBytes + ch <- c.vmDiskWriteBytes + ch <- c.vmDiskReadOps + ch <- c.vmDiskWriteOps + ch <- c.vmNetworkRxBytes + ch <- c.vmNetworkTxBytes + ch <- c.vmNetworkRxPkts + ch <- c.vmNetworkTxPkts + ch <- c.vmUptime +} + +// Collect implements the prometheus.Collector interface +func (c *LibvirtCollector) Collect(ch chan<- prometheus.Metric) { + c.mutex.Lock() + defer c.mutex.Unlock() + + // Check connection health + alive, err := c.conn.IsAlive() + if err != nil || !alive { + log.Printf("Connection to libvirt lost, reconnecting...") + c.conn.Close() + + conn, err := libvirt.NewConnect(c.uri) + if err != nil { + log.Printf("Failed to reconnect to libvirt: %v", err) + return + } + c.conn = conn + } + + // Get all domains + domains, err := c.conn.ListAllDomains(libvirt.CONNECT_LIST_DOMAINS_ACTIVE | libvirt.CONNECT_LIST_DOMAINS_INACTIVE) + if err != nil { + log.Printf("Failed to list domains: %v", err) + return + } + defer func() { + for _, domain := range domains { + domain.Free() + } + }() + + for _, domain := range domains { + c.collectDomainMetrics(ch, &domain) + } +} + +func (c *LibvirtCollector) collectDomainMetrics(ch chan<- prometheus.Metric, domain *libvirt.Domain) { + domainInfo, err := domain.GetInfo() + if err != nil { + log.Printf("Failed to get domain info: %v", err) + return + } + + domainName, err := domain.GetName() + if err != nil { + log.Printf("Failed to get domain name: %v", err) + return + } + + domainUUID, err := domain.GetUUIDString() + if err != nil { + log.Printf("Failed to get domain UUID: %v", err) + return + } + + // VM status metric + status := 0.0 + if domainInfo.State == libvirt.DOMAIN_RUNNING { + status = 1.0 + } + ch <- prometheus.MustNewConstMetric(c.vmStatus, prometheus.GaugeValue, status, domainName, domainUUID) + + // CPU time metric (convert from nanoseconds to seconds) + ch <- prometheus.MustNewConstMetric(c.vmCPUTime, prometheus.CounterValue, float64(domainInfo.CpuTime)/1e9, domainName, domainUUID) + + // Memory metrics + ch <- prometheus.MustNewConstMetric(c.vmMemoryCurrent, prometheus.GaugeValue, float64(domainInfo.Memory)*1024, domainName, domainUUID) + ch <- prometheus.MustNewConstMetric(c.vmMemoryMax, prometheus.GaugeValue, float64(domainInfo.MaxMem)*1024, domainName, domainUUID) + + // Only collect additional metrics for running domains + if domainInfo.State == libvirt.DOMAIN_RUNNING { + c.collectRunningDomainMetrics(ch, domain, domainName, domainUUID) + } +} + +func (c *LibvirtCollector) collectRunningDomainMetrics(ch chan<- prometheus.Metric, domain *libvirt.Domain, domainName, domainUUID string) { + // Collect disk I/O statistics for common block devices + blockDevices := []string{"vda", "vdb", "hda", "hdb", "sda", "sdb"} + for _, device := range blockDevices { + stats, err := domain.BlockStats(device) + if err == nil { + ch <- prometheus.MustNewConstMetric(c.vmDiskReadBytes, prometheus.CounterValue, float64(stats.RdBytes), domainName, domainUUID, device) + ch <- prometheus.MustNewConstMetric(c.vmDiskWriteBytes, prometheus.CounterValue, float64(stats.WrBytes), domainName, domainUUID, device) + ch <- prometheus.MustNewConstMetric(c.vmDiskReadOps, prometheus.CounterValue, float64(stats.RdReq), domainName, domainUUID, device) + ch <- prometheus.MustNewConstMetric(c.vmDiskWriteOps, prometheus.CounterValue, float64(stats.WrReq), domainName, domainUUID, device) + } + } + + // Collect network I/O statistics for common network interfaces + netInterfaces := []string{"vnet0", "vnet1", "eth0", "eth1"} + for _, iface := range netInterfaces { + stats, err := domain.InterfaceStats(iface) + if err == nil { + ch <- prometheus.MustNewConstMetric(c.vmNetworkRxBytes, prometheus.CounterValue, float64(stats.RxBytes), domainName, domainUUID, iface) + ch <- prometheus.MustNewConstMetric(c.vmNetworkTxBytes, prometheus.CounterValue, float64(stats.TxBytes), domainName, domainUUID, iface) + ch <- prometheus.MustNewConstMetric(c.vmNetworkRxPkts, prometheus.CounterValue, float64(stats.RxPackets), domainName, domainUUID, iface) + ch <- prometheus.MustNewConstMetric(c.vmNetworkTxPkts, prometheus.CounterValue, float64(stats.TxPackets), domainName, domainUUID, iface) + } + } + + // Collect uptime (simplified - using current time minus start time) + domainTime, _, err := domain.GetTime(0) + if err == nil { + uptime := time.Since(time.Unix(int64(domainTime/1000), 0)).Seconds() + ch <- prometheus.MustNewConstMetric(c.vmUptime, prometheus.GaugeValue, uptime, domainName, domainUUID) + } +} + +// Close closes the libvirt connection +func (c *LibvirtCollector) Close() { + if c.conn != nil { + c.conn.Close() + } +} \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..fbc56b5 --- /dev/null +++ b/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "flag" + "log" + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var ( + libvirtURI = flag.String("libvirt.uri", "qemu:///system", "Libvirt connection URI") + listenAddr = flag.String("web.listen-address", ":9177", "Address to listen on for web interface and telemetry") + metricsPath = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics") +) + +func main() { + flag.Parse() + + log.Printf("Starting uos-libvirtd-exporter %s", version) + log.Printf("Libvirt URI: %s", *libvirtURI) + log.Printf("Listening on: %s", *listenAddr) + log.Printf("Metrics path: %s", *metricsPath) + + // Create libvirt collector + collector, err := NewLibvirtCollector(*libvirtURI) + if err != nil { + log.Fatalf("Failed to create libvirt collector: %v", err) + } + + // Register collector + prometheus.MustRegister(collector) + + // Setup HTTP handlers + http.Handle(*metricsPath, promhttp.Handler()) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(` + UOS Libvirt Exporter + +

UOS Libvirt Exporter

+

Metrics

+

Build version: ` + version + `

+ + `)) + }) + + // Setup signal handling + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + go func() { + <-sigChan + log.Println("Shutting down...") + collector.Close() + os.Exit(0) + }() + + // Start HTTP server + log.Printf("Starting HTTP server on %s", *listenAddr) + if err := http.ListenAndServe(*listenAddr, nil); err != nil { + log.Fatalf("Failed to start HTTP server: %v", err) + } +} + +var version = "dev" \ No newline at end of file -- Gitee