From a3bb10846ac24786b55883339ef6a514184e5840 Mon Sep 17 00:00:00 2001
From: Mohammed Naser <mnaser@vexxhost.com>
Date: Tue, 31 Mar 2020 18:58:40 -0400
Subject: [PATCH] initial commit

Change-Id: I2ec47c1f516a8c930360f0f13a11c651595e6e1c
---
 .zuul.yaml                 |  30 ++
 Dockerfile                 |  14 +
 bindep.txt                 |   3 +
 collectors/domain_stats.go | 558 +++++++++++++++++++++++++++++++++++++
 go.mod                     |  11 +
 go.sum                     |  77 +++++
 libvirtd_exporter.go       |  78 ++++++
 7 files changed, 771 insertions(+)
 create mode 100644 .zuul.yaml
 create mode 100644 Dockerfile
 create mode 100644 bindep.txt
 create mode 100644 collectors/domain_stats.go
 create mode 100644 go.mod
 create mode 100644 go.sum
 create mode 100644 libvirtd_exporter.go

diff --git a/.zuul.yaml b/.zuul.yaml
new file mode 100644
index 0000000..d0cc6be
--- /dev/null
+++ b/.zuul.yaml
@@ -0,0 +1,30 @@
+- job:
+    name: libvirtd_exporter:image:build
+    parent: vexxhost-build-docker-image
+    provides: libvirtd_exporter:image
+    vars: &libvirtd_exporter_images
+      docker_images:
+        - context: .
+          repository: vexxhost/libvirtd-exporter
+
+- job:
+    name: libvirtd_exporter:image:upload
+    parent: vexxhost-upload-docker-image
+    provides: libvird_exporter:image
+    vars: *libvirtd_exporter_images
+
+- job:
+    name: libvirtd_exporter:image:promote
+    parent: vexxhost-promote-docker-image
+    vars: *libvirtd_exporter_images
+
+- project:
+    check:
+      jobs:
+        - libvirtd_exporter:image:build
+    gate:
+      jobs:
+        - libvirtd_exporter:image:upload
+    promote:
+      jobs:
+        - libvirtd_exporter:image:promote
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..134a961
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,14 @@
+FROM golang:1.13.5 AS builder
+WORKDIR /go/src/app
+COPY . .
+RUN apt-get update && \
+    apt-get -y install libvirt-dev && \
+    apt-get clean all
+RUN go build
+
+FROM golang:1.13.5
+RUN apt-get update && \
+    apt-get -y install libvirt0 && \
+    apt-get clean all
+COPY --from=builder /go/src/app/libvirtd_exporter /libvirtd_exporter
+ENTRYPOINT ["/libvirtd_exporter"]
diff --git a/bindep.txt b/bindep.txt
new file mode 100644
index 0000000..f7189b1
--- /dev/null
+++ b/bindep.txt
@@ -0,0 +1,3 @@
+gcc
+pkg-config
+libvirt-dev
diff --git a/collectors/domain_stats.go b/collectors/domain_stats.go
new file mode 100644
index 0000000..a8c07d5
--- /dev/null
+++ b/collectors/domain_stats.go
@@ -0,0 +1,558 @@
+// Copyright 2019 VEXXHOST, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package collectors
+
+import (
+	"encoding/xml"
+	"strconv"
+	"time"
+
+	"github.com/libvirt/libvirt-go"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/common/log"
+)
+
+type DomainStatsCollector struct {
+	prometheus.Collector
+
+	Connection *libvirt.Connect
+	Nova       bool
+
+	DomainSeconds *prometheus.Desc
+
+	DomainDomainState       *prometheus.Desc
+	DomainDomainStateReason *prometheus.Desc
+
+	DomainCPUTime   *prometheus.Desc
+	DomainCPUUser   *prometheus.Desc
+	DomainCPUSystem *prometheus.Desc
+
+	DomainBalloonCurrent *prometheus.Desc
+	DomainBalloonMaximum *prometheus.Desc
+
+	DomainVcpuState *prometheus.Desc
+	DomainVcpuTime  *prometheus.Desc
+
+	DomainNetRxBytes *prometheus.Desc
+	DomainNetRxPkts  *prometheus.Desc
+	DomainNetRxErrs  *prometheus.Desc
+	DomainNetRxDrop  *prometheus.Desc
+	DomainNetTxBytes *prometheus.Desc
+	DomainNetTxPkts  *prometheus.Desc
+	DomainNetTxErrs  *prometheus.Desc
+	DomainNetTxDrop  *prometheus.Desc
+
+	DomainBlockRdReqs     *prometheus.Desc
+	DomainBlockRdBytes    *prometheus.Desc
+	DomainBlockRdTimes    *prometheus.Desc
+	DomainBlockWrReqs     *prometheus.Desc
+	DomainBlockWrBytes    *prometheus.Desc
+	DomainBlockWrTimes    *prometheus.Desc
+	DomainBlockFlReqs     *prometheus.Desc
+	DomainBlockFlTimes    *prometheus.Desc
+	DomainBlockErrors     *prometheus.Desc
+	DomainBlockAllocation *prometheus.Desc
+	DomainBlockCapacity   *prometheus.Desc
+	DomainBlockPhysical   *prometheus.Desc
+}
+
+type NovaFlavorMetadata struct {
+	Name string `xml:"name,attr"`
+}
+
+type NovaOwnerMetadata struct {
+	UUID string `xml:"uuid,attr"`
+}
+
+type NovaMetadata struct {
+	Seconds      float64            `xml:"omitempty"`
+	CreationTime string             `xml:"creationTime"`
+	Flavor       NovaFlavorMetadata `xml:"flavor"`
+	User         NovaOwnerMetadata  `xml:"owner>user"`
+	Project      NovaOwnerMetadata  `xml:"owner>project"`
+}
+
+// nolint:funlen
+func NewDomainStatsCollector(uri string, nova bool) (*DomainStatsCollector, error) {
+	conn, err := libvirt.NewConnect(uri)
+	if err != nil {
+		return nil, err
+	}
+
+	return &DomainStatsCollector{
+		Connection: conn,
+		Nova:       nova,
+
+		DomainSeconds: prometheus.NewDesc(
+			"libvirtd_domain_seconds",
+			"seconds since creation time",
+			[]string{"uuid", "instance_type", "user_id", "project_id"}, nil,
+		),
+
+		DomainDomainState: prometheus.NewDesc(
+			"libvirtd_domain_domain_state",
+			"state of the VM (virDomainState enum)",
+			[]string{"uuid"}, nil,
+		),
+		DomainDomainStateReason: prometheus.NewDesc(
+			"libvirtd_domain_domain_state_reason",
+			"reason for entering given state (virDomain*Reason enum)",
+			[]string{"uuid"}, nil,
+		),
+
+		DomainCPUTime: prometheus.NewDesc(
+			"libvirtd_domain_cpu_time",
+			"total cpu time spent for this domain in nanoseconds",
+			[]string{"uuid"}, nil,
+		),
+		DomainCPUUser: prometheus.NewDesc(
+			"libvirtd_domain_cpu_user",
+			"user cpu time spent in nanoseconds",
+			[]string{"uuid"}, nil,
+		),
+		DomainCPUSystem: prometheus.NewDesc(
+			"libvirtd_domain_cpu_system",
+			"system cpu time spent in nanoseconds",
+			[]string{"uuid"}, nil,
+		),
+
+		DomainBalloonCurrent: prometheus.NewDesc(
+			"libvirtd_domain_balloon_current",
+			"the memory in kiB currently used",
+			[]string{"uuid"}, nil,
+		),
+		DomainBalloonMaximum: prometheus.NewDesc(
+			"libvirtd_domain_balloon_maximum",
+			"the maximum memory in kiB allowed",
+			[]string{"uuid"}, nil,
+		),
+
+		DomainVcpuState: prometheus.NewDesc(
+			"libvirtd_domain_vcpu_state",
+			"state of the virtual CPU (virVcpuState enum)",
+			[]string{"uuid", "vcpu"}, nil,
+		),
+		DomainVcpuTime: prometheus.NewDesc(
+			"libvirtd_domain_vcpu_time",
+			"virtual cpu time spent",
+			[]string{"uuid", "vcpu"}, nil,
+		),
+
+		DomainNetRxBytes: prometheus.NewDesc(
+			"libvirtd_domain_net_rx_bytes",
+			"bytes received",
+			[]string{"uuid", "interface"}, nil,
+		),
+		DomainNetRxPkts: prometheus.NewDesc(
+			"libvirtd_domain_net_rx_packets",
+			"packets received",
+			[]string{"uuid", "interface"}, nil,
+		),
+		DomainNetRxErrs: prometheus.NewDesc(
+			"libvirtd_domain_net_rx_errors",
+			"receive errors",
+			[]string{"uuid", "interface"}, nil,
+		),
+		DomainNetRxDrop: prometheus.NewDesc(
+			"libvirtd_domain_net_rx_drop",
+			"receive packets dropped",
+			[]string{"uuid", "interface"}, nil,
+		),
+		DomainNetTxBytes: prometheus.NewDesc(
+			"libvirtd_domain_net_tx_bytes",
+			"bytes transmitted",
+			[]string{"uuid", "interface"}, nil,
+		),
+		DomainNetTxPkts: prometheus.NewDesc(
+			"libvirtd_domain_net_tx_packets",
+			"packets transmitted",
+			[]string{"uuid", "interface"}, nil,
+		),
+		DomainNetTxErrs: prometheus.NewDesc(
+			"libvirtd_domain_net_tx_errors",
+			"transmission errors",
+			[]string{"uuid", "interface"}, nil,
+		),
+		DomainNetTxDrop: prometheus.NewDesc(
+			"libvirtd_domain_net_tx_drop",
+			"transmit packets dropped",
+			[]string{"uuid", "interface"}, nil,
+		),
+
+		DomainBlockRdReqs: prometheus.NewDesc(
+			"libvirtd_domain_block_read_requests",
+			"number of read requests",
+			[]string{"uuid", "device", "path"}, nil,
+		),
+		DomainBlockRdBytes: prometheus.NewDesc(
+			"libvirtd_domain_block_read_bytes",
+			"number of read bytes",
+			[]string{"uuid", "device", "path"}, nil,
+		),
+		DomainBlockRdTimes: prometheus.NewDesc(
+			"libvirtd_domain_block_read_times",
+			"total time (ns) spent on reads",
+			[]string{"uuid", "device", "path"}, nil,
+		),
+		DomainBlockWrReqs: prometheus.NewDesc(
+			"libvirtd_domain_block_write_requests",
+			"number of written requests",
+			[]string{"uuid", "device", "path"}, nil,
+		),
+		DomainBlockWrBytes: prometheus.NewDesc(
+			"libvirtd_domain_block_write_bytes",
+			"number of written bytes",
+			[]string{"uuid", "device", "path"}, nil,
+		),
+		DomainBlockWrTimes: prometheus.NewDesc(
+			"libvirtd_domain_block_write_times",
+			"total time (ns) spent on writes",
+			[]string{"uuid", "device", "path"}, nil,
+		),
+		DomainBlockFlReqs: prometheus.NewDesc(
+			"libvirtd_domain_block_flush_requests",
+			"total flush requests",
+			[]string{"uuid", "device", "path"}, nil,
+		),
+		DomainBlockFlTimes: prometheus.NewDesc(
+			"libvirtd_domain_block_flush_times",
+			"total time (ns) spent on cache flushing",
+			[]string{"uuid", "device", "path"}, nil,
+		),
+		DomainBlockAllocation: prometheus.NewDesc(
+			"libvirtd_domain_block_allocation",
+			"offset of the highest written sector",
+			[]string{"uuid", "device", "path"}, nil,
+		),
+		DomainBlockCapacity: prometheus.NewDesc(
+			"libvirtd_domain_block_capacity",
+			"logical size in bytes of the block device backing image",
+			[]string{"uuid", "device", "path"}, nil,
+		),
+		DomainBlockPhysical: prometheus.NewDesc(
+			"libvirtd_domain_block_physical",
+			"physical size in bytes of the container of the backing image",
+			[]string{"uuid", "device", "path"}, nil,
+		),
+	}, nil
+}
+
+func (c *DomainStatsCollector) Describe(ch chan<- *prometheus.Desc) {
+	c.describeNova(ch)
+	c.describeState(ch)
+	c.describeCPU(ch)
+	c.describeBalloon(ch)
+	c.describeVcpu(ch)
+	c.describeNet(ch)
+	c.describeBlock(ch)
+}
+
+func (c *DomainStatsCollector) describeNova(ch chan<- *prometheus.Desc) {
+	if c.Nova {
+		ch <- c.DomainSeconds
+	}
+}
+
+func (c *DomainStatsCollector) describeState(ch chan<- *prometheus.Desc) {
+	ch <- c.DomainDomainState
+	ch <- c.DomainDomainStateReason
+}
+
+func (c *DomainStatsCollector) describeCPU(ch chan<- *prometheus.Desc) {
+	ch <- c.DomainCPUTime
+	ch <- c.DomainCPUUser
+	ch <- c.DomainCPUSystem
+}
+
+func (c *DomainStatsCollector) describeBalloon(ch chan<- *prometheus.Desc) {
+	ch <- c.DomainBalloonCurrent
+	ch <- c.DomainBalloonMaximum
+}
+
+func (c *DomainStatsCollector) describeVcpu(ch chan<- *prometheus.Desc) {
+	ch <- c.DomainVcpuState
+	ch <- c.DomainVcpuTime
+}
+
+func (c *DomainStatsCollector) describeNet(ch chan<- *prometheus.Desc) {
+	ch <- c.DomainNetRxBytes
+	ch <- c.DomainNetRxPkts
+	ch <- c.DomainNetRxErrs
+	ch <- c.DomainNetRxDrop
+	ch <- c.DomainNetTxBytes
+	ch <- c.DomainNetTxPkts
+	ch <- c.DomainNetTxErrs
+	ch <- c.DomainNetTxDrop
+}
+
+func (c *DomainStatsCollector) describeBlock(ch chan<- *prometheus.Desc) {
+	ch <- c.DomainBlockRdReqs
+	ch <- c.DomainBlockRdBytes
+	ch <- c.DomainBlockRdTimes
+	ch <- c.DomainBlockWrReqs
+	ch <- c.DomainBlockWrBytes
+	ch <- c.DomainBlockWrTimes
+	ch <- c.DomainBlockFlReqs
+	ch <- c.DomainBlockFlTimes
+	ch <- c.DomainBlockAllocation
+	ch <- c.DomainBlockCapacity
+	ch <- c.DomainBlockPhysical
+}
+
+func (c *DomainStatsCollector) Collect(ch chan<- prometheus.Metric) {
+	stats, err := c.Connection.GetAllDomainStats(
+		[]*libvirt.Domain{},
+		libvirt.DOMAIN_STATS_STATE|libvirt.DOMAIN_STATS_CPU_TOTAL|libvirt.DOMAIN_STATS_BALLOON|
+			libvirt.DOMAIN_STATS_VCPU|libvirt.DOMAIN_STATS_INTERFACE|libvirt.DOMAIN_STATS_BLOCK,
+		0,
+	)
+
+	if err != nil {
+		log.Errorln(err)
+		return
+	}
+
+	for _, stat := range stats {
+		uuid, err := stat.Domain.GetUUIDString()
+		if err != nil {
+			log.Errorln(err)
+			continue
+		}
+
+		c.collectNova(uuid, stat, ch)
+		c.collectState(uuid, stat, ch)
+		c.collectCPU(uuid, stat, ch)
+		c.collectBalloon(uuid, stat, ch)
+		c.collectVcpu(uuid, stat, ch)
+		c.collectNet(uuid, stat, ch)
+		c.collectBlock(uuid, stat, ch)
+	}
+}
+
+func (c *DomainStatsCollector) collectNova(uuid string, stat libvirt.DomainStats, ch chan<- prometheus.Metric) {
+	if c.Nova {
+		metadata, err := c.getNovaMetadata(stat.Domain)
+
+		if err != nil {
+			log.Errorln(err)
+		} else {
+			ch <- prometheus.MustNewConstMetric(
+				c.DomainSeconds,
+				prometheus.CounterValue,
+				metadata.Seconds, uuid, metadata.Flavor.Name, metadata.User.UUID, metadata.Project.UUID,
+			)
+		}
+	}
+}
+
+func (c *DomainStatsCollector) collectState(uuid string, stat libvirt.DomainStats, ch chan<- prometheus.Metric) {
+	ch <- prometheus.MustNewConstMetric(
+		c.DomainDomainState,
+		prometheus.GaugeValue,
+		float64(stat.State.State), uuid,
+	)
+	ch <- prometheus.MustNewConstMetric(
+		c.DomainDomainStateReason,
+		prometheus.GaugeValue,
+		float64(stat.State.Reason), uuid,
+	)
+}
+
+func (c *DomainStatsCollector) collectCPU(uuid string, stat libvirt.DomainStats, ch chan<- prometheus.Metric) {
+	if stat.Cpu != nil {
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainCPUTime,
+			prometheus.CounterValue,
+			float64(stat.Cpu.Time), uuid,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainCPUUser,
+			prometheus.CounterValue,
+			float64(stat.Cpu.User), uuid,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainCPUSystem,
+			prometheus.CounterValue,
+			float64(stat.Cpu.System), uuid,
+		)
+	}
+}
+
+func (c *DomainStatsCollector) collectBalloon(uuid string, stat libvirt.DomainStats, ch chan<- prometheus.Metric) {
+	ch <- prometheus.MustNewConstMetric(
+		c.DomainBalloonCurrent,
+		prometheus.GaugeValue,
+		float64(stat.Balloon.Current), uuid,
+	)
+	ch <- prometheus.MustNewConstMetric(
+		c.DomainBalloonMaximum,
+		prometheus.GaugeValue,
+		float64(stat.Balloon.Maximum), uuid,
+	)
+}
+
+func (c *DomainStatsCollector) collectVcpu(uuid string, stat libvirt.DomainStats, ch chan<- prometheus.Metric) {
+	for vcpu, vcpuStats := range stat.Vcpu {
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainVcpuState,
+			prometheus.GaugeValue,
+			float64(vcpuStats.State), uuid, strconv.Itoa(vcpu),
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainVcpuTime,
+			prometheus.CounterValue,
+			float64(vcpuStats.Time), uuid, strconv.Itoa(vcpu),
+		)
+	}
+}
+
+func (c *DomainStatsCollector) collectNet(uuid string, stat libvirt.DomainStats, ch chan<- prometheus.Metric) {
+	for _, netStats := range stat.Net {
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainNetRxBytes,
+			prometheus.CounterValue,
+			float64(netStats.RxBytes), uuid, netStats.Name,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainNetRxPkts,
+			prometheus.CounterValue,
+			float64(netStats.RxPkts), uuid, netStats.Name,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainNetRxErrs,
+			prometheus.CounterValue,
+			float64(netStats.RxErrs), uuid, netStats.Name,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainNetRxDrop,
+			prometheus.CounterValue,
+			float64(netStats.RxDrop), uuid, netStats.Name,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainNetTxBytes,
+			prometheus.CounterValue,
+			float64(netStats.TxBytes), uuid, netStats.Name,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainNetTxPkts,
+			prometheus.CounterValue,
+			float64(netStats.TxPkts), uuid, netStats.Name,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainNetTxErrs,
+			prometheus.CounterValue,
+			float64(netStats.TxErrs), uuid, netStats.Name,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainNetTxDrop,
+			prometheus.GaugeValue,
+			float64(netStats.TxDrop), uuid, netStats.Name,
+		)
+	}
+}
+
+func (c *DomainStatsCollector) collectBlock(uuid string, stat libvirt.DomainStats, ch chan<- prometheus.Metric) {
+	for device, blockStats := range stat.Block {
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainBlockRdReqs,
+			prometheus.CounterValue,
+			float64(blockStats.RdReqs), uuid, strconv.Itoa(device), blockStats.Path,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainBlockRdBytes,
+			prometheus.CounterValue,
+			float64(blockStats.RdBytes), uuid, strconv.Itoa(device), blockStats.Path,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainBlockRdTimes,
+			prometheus.CounterValue,
+			float64(blockStats.RdTimes), uuid, strconv.Itoa(device), blockStats.Path,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainBlockWrReqs,
+			prometheus.CounterValue,
+			float64(blockStats.RdReqs), uuid, strconv.Itoa(device), blockStats.Path,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainBlockWrBytes,
+			prometheus.CounterValue,
+			float64(blockStats.RdBytes), uuid, strconv.Itoa(device), blockStats.Path,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainBlockWrTimes,
+			prometheus.CounterValue,
+			float64(blockStats.RdTimes), uuid, strconv.Itoa(device), blockStats.Path,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainBlockFlReqs,
+			prometheus.CounterValue,
+			float64(blockStats.FlReqs), uuid, strconv.Itoa(device), blockStats.Path,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainBlockFlTimes,
+			prometheus.CounterValue,
+			float64(blockStats.FlTimes), uuid, strconv.Itoa(device), blockStats.Path,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainBlockAllocation,
+			prometheus.GaugeValue,
+			float64(blockStats.Allocation), uuid, strconv.Itoa(device), blockStats.Path,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainBlockCapacity,
+			prometheus.GaugeValue,
+			float64(blockStats.Capacity), uuid, strconv.Itoa(device), blockStats.Path,
+		)
+		ch <- prometheus.MustNewConstMetric(
+			c.DomainBlockPhysical,
+			prometheus.GaugeValue,
+			float64(blockStats.Physical), uuid, strconv.Itoa(device), blockStats.Path,
+		)
+	}
+}
+
+func (c *DomainStatsCollector) getNovaMetadata(domain *libvirt.Domain) (*NovaMetadata, error) {
+	data, err := domain.GetMetadata(
+		libvirt.DOMAIN_METADATA_ELEMENT,
+		"http://openstack.org/xmlns/libvirt/nova/1.0",
+		libvirt.DOMAIN_AFFECT_LIVE,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	m := &NovaMetadata{}
+	err = xml.Unmarshal([]byte(data), &m)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// Parse creationTime from Nova format: "%Y-%m-%d %H:%M:%S"
+	layout := "2006-01-02 15:04:05"
+	creationTime, err := time.Parse(layout, m.CreationTime)
+
+	if err != nil {
+		return nil, err
+	}
+
+	m.Seconds = time.Since(creationTime).Seconds()
+
+	return m, nil
+}
+
+func (c *DomainStatsCollector) Close() {
+	c.Connection.Close()
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..26c9267
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,11 @@
+module opendev.org/vexxhost/libvirtd_exporter
+
+go 1.13
+
+require (
+	github.com/libvirt/libvirt-go v5.10.0+incompatible
+	github.com/prometheus/client_golang v1.0.0
+	github.com/prometheus/common v0.7.0
+	github.com/vexxhost/libvirtd_exporter v0.0.0-20200314024639-aa298d3ca901
+	gopkg.in/alecthomas/kingpin.v2 v2.2.6
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..fcf2695
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,77 @@
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+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/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+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.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
+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/libvirt/libvirt-go v5.10.0+incompatible h1:01fwkdUHH2hk4YyFNCr48OvSGqXYLzp9cofUpeyeLNc=
+github.com/libvirt/libvirt-go v5.10.0+incompatible/go.mod h1:34zsnB4iGeOv7Byj6qotuW8Ya4v4Tr43ttjz/F0wjLE=
+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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
+github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+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/vexxhost/libvirtd_exporter v0.0.0-20200314024639-aa298d3ca901 h1:+Q2+NsO677P7tqUldcesbGpH3fhganXHSiUmdhzTQCc=
+github.com/vexxhost/libvirtd_exporter v0.0.0-20200314024639-aa298d3ca901/go.mod h1:vOV8Sa6zGZrMV8PKxz21ZbSeErrMYVviINDCov0EPto=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+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/sys v0.0.0-20180905080454-ebe1bf3edb33/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+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=
diff --git a/libvirtd_exporter.go b/libvirtd_exporter.go
new file mode 100644
index 0000000..63ae790
--- /dev/null
+++ b/libvirtd_exporter.go
@@ -0,0 +1,78 @@
+// Copyright 2019 VEXXHOST, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	"net/http"
+
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
+	"github.com/prometheus/common/log"
+	"github.com/prometheus/common/version"
+	"gopkg.in/alecthomas/kingpin.v2"
+
+	"github.com/vexxhost/libvirtd_exporter/collectors"
+)
+
+func main() {
+	var (
+		listenAddress = kingpin.Flag(
+			"web.listen-address",
+			"Address on which to expose metrics and web interface.",
+		).Default(":9474").String()
+		metricsPath = kingpin.Flag(
+			"web.telemetry-path",
+			"Path under which to expose metrics.",
+		).Default("/metrics").String()
+		libvirtURI = kingpin.Flag(
+			"libvirt.uri",
+			"Libvirt Connection URI",
+		).Default("qemu:///system").String()
+		libvirtNova = kingpin.Flag(
+			"libvirt.nova",
+			"Parse Libvirt Nova metadata",
+		).Bool()
+	)
+
+	kingpin.Version(version.Print("libvirtd_exporter"))
+	kingpin.HelpFlag.Short('h')
+	kingpin.Parse()
+
+	domainStats, err := collectors.NewDomainStatsCollector(*libvirtURI, *libvirtNova)
+	if err != nil {
+		log.Fatalln(err)
+	}
+
+	prometheus.MustRegister(domainStats)
+
+	defer domainStats.Close()
+
+	http.Handle("/metrics", promhttp.Handler())
+	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(`<html>
+			<head><title>Libvirtd Exporter</title></head>
+			<body>
+			<h1>Libvirtd Exporter</h1>
+			<p><a href="` + *metricsPath + `">Metrics</a></p>
+			<h2>Build</h2>
+			<pre>` + version.Info() + ` ` + version.BuildContext() + `</pre>
+			</body>
+			</body>
+			</html>`))
+	})
+
+	log.Infoln("Listening on", *listenAddress)
+	log.Fatal(http.ListenAndServe(*listenAddress, nil))
+}