Browse Source

first commit

GTong 4 months ago
commit
5b03dabd1d

+ 22 - 0
.vscode/launch.json

@@ -0,0 +1,22 @@
+{
+
+          // 使用 IntelliSense 了解相关属性。
+    
+          // 悬停以查看现有属性的描述。
+    
+          // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
+    
+             "version": "0.2.0",
+          "configurations": [
+                {
+                      "name": "Launch Package",
+                      "type": "go",
+                      "request": "launch",
+                      "mode": "auto",
+                      "program": "${workspaceFolder}/main.go",
+                      "args": ["--custom.metrics", "xugu_example.toml","--database.dsn", "IP=10.28.20.150;DB=SYSTEM;User=SYSDBA;PWD=SYSDBA;Port=13141;AUTO_COMMIT=on;CHAR_SET=UTF8" 
+                    ]
+                }
+          ]
+    
+    }

+ 32 - 0
BIN/xugu_example.toml

@@ -0,0 +1,32 @@
+
+[[metric]]  # 定义一个指标配置
+# 指标的上下文或分类,这里为 "test" 表示测试用途
+context = "trans_xugu"  
+
+labels = [ "server_time", "node_id" ] 
+
+# SQL 查询,用于从 DUAL 表中选择常量值,返回两个值和两个标签
+request = "SELECT CURR_T as server_time,NODEID node_id ,ACT_TRANS_NUM  act ,LOCK_WAIT_N lwn FROM sys_run_info;"  
+  
+# 描述 value_1 为计数器类型,始终返回 1,value_2 为仪表类型,始终返回 2
+metricsdesc = { act = "当前节点活动事务数量", lwn ="锁等待数量" }  
+  
+# 指定 value_1 的类型为计数器(counter),value_2 默认为仪表(gauge)
+metricstype = { act = "gauge" , lwn = "gauge"}  
+        #计数器(counter)指的是一个只能增加或重置的值,常用于表示事件的发生次数。
+        #仪表(gauge)则是一个可以增减的值,通常用于表示某个瞬时状态或度量(如温度、内存使用量等)。
+
+
+[[metric]] # 定义一个指标配置
+context = "DISK_xugu" # 指标的上下文或分类,这里是 "test_histo",表示这是一个用于测试的直方图
+# SQL 查询,选择了多个字段来模拟直方图的不同桶(buckets)和相关的计数值
+request = "SELECT CURR_T as server_time,NODEID node_id , DISK_W_BYTES  / (1024 * 1024)  AS data ,DISK_R_BYTES / (1024 * 1024)  AS drb ,45 as count FROM sys_run_info;"
+# 描述字段 data 为直方图类型,表示数据字段的所有值的总和
+metricsdesc = { data = "Histogram - sum total of all values in the data field." }
+# 指定 data 字段的类型为直方图(histogram)
+metricstype = { data = "histogram" }
+
+labels = ["server_time", "node_id" ]
+metricsbuckets = { data = { le_20 = "20", le_40 = "40", le_60 = "60", le_80 = "80" } }
+
+

BIN
BIN/xugu_exporter.exe


BIN
BIN/xugu_exporter.zip


BIN
BIN/xugu_exporter_linux_amd64


BIN
BIN/xugu_exporter_linux_arm64


+ 175 - 0
BIN/xugu_expoter使用手册.md

@@ -0,0 +1,175 @@
+
+# 数据库连接配置
+## 数据库连接导入系统环境(当前终端)
+```shell
+#环境导入方式
+#Linux
+export DATA_SOURCE_NAME="IP=10.28.20.150;DB=SYSTEM;User=SYSDBA;PWD=SYSDBA;Port=13141;AUTO_COMMIT=on;CHAR_SET=UTF8"
+
+#Windows
+$env:DATA_SOURCE_NAME="IP=10.28.20.150;DB=SYSTEM;User=SYSDBA;PWD=SYSDBA;Port=13141;AUTO_COMMIT=on;CHAR_SET=UTF8"
+
+```
+
+## 通过程序运行传参导入数据库连接
+```shell
+./程序名 --database.dsn "IP=10.28.20.150;DB=SYSTEM;User=SYSDBA;PWD=SYSDBA;Port=13141;AUTO_COMMIT=on;CHAR_SET=UTF8" 
+```
+
+
+---
+
+# 默认配置文件导入
+	不需要必须导入
+### 方式一
+```
+与程序同级目录下的default-metrics.toml文件
+```
+
+### 方式二
+#### 导入到系统环境
+```shell
+#windwos
+$env:DEFAULT_METRICS="xugu_example.toml"
+#linux
+export  DEFAULT_METRICS="xugu_example.toml"
+```
+
+---
+
+
+# 自定义配置文件导入
+## 通过运行程序传参导入
+```shell
+./程序名  --custom.metrics "xugu_example.toml"
+
+#导入多个,以逗号分隔
+./程序名  --custom.metrics "xugu_example.toml,xugu_example2.toml"
+```
+
+## 通过导入环境导入
+```shell 
+#windows
+$env:CUSTOM_METRICS="xugu_example.toml"
+ 
+#linux
+export CUSTOM_METRICS="xugu_example.toml"
+```
+
+---
+
+## 其他运行参数
+Usage of xugu_exporter:
+  --log.format value
+        如果设置,使用syslog记录器或JSON格式记录日志。例如:logger:syslog?appname=bob&local=7 或 logger:stdout?json=true。默认输出到stderr。
+  --log.level value
+        仅记录给定严重级别或以上的消息。有效级别:[debug, info, warn, error, fatal]。
+  --custom.metrics string
+        包含各种自定义指标的文件,格式为toml或yaml。
+  --default.metrics string
+        默认指标文件,格式为toml或yaml。
+  --web.systemd-socket
+        使用systemd套接字激活监听器,而不是端口监听器(仅限Linux)。
+  --web.listen-address value
+        Web服务器监听的地址和端口。默认值:0.0.0.0:9161。
+  --web.telemetry-path value
+        设置Prometheus的采集路径。默认值:/metrics。
+  --web.disable-exporter-metrics
+        禁用导出器指标。
+
+---
+
+## 指标配置文件编写
+	文件后缀名:文件名.toml 
+	
+```toml
+[[metric]]  # 定义一个指标配置
+# 指标的上下文或分类,这里为 "test" 表示测试用途
+context = "test"  
+
+# SQL 查询,用于从 DUAL 表中选择常量值,返回两个值和两个标签
+request = "SELECT 1 as value_1, 2 as value_2, 'First label' as label_1, 'Second label' as label_2 FROM DUAL"  
+  
+# 描述 value_1 为计数器类型,始终返回 1,value_2 为仪表类型,始终返回 2
+metricsdesc = { value_1 = "简单示例,始终返回 1 作为计数器(counter).", value_2 = "同样的示例,始终返回 2 作为仪表(gauge)" }  
+  
+# 指定 value_1 的类型为计数器(counter),value_2 默认为仪表(gauge)
+metricstype = { value_1 = "counter" }  
+        #计数器(counter)指的是一个只能增加或重置的值,常用于表示事件的发生次数。
+        #仪表(gauge)则是一个可以增减的值,通常用于表示某个瞬时状态或度量(如温度、内存使用量等)。
+
+#输出
+# HELP xugudb_test_value_1 Simple example returning always 1 as counter.
+# TYPE xugudb_test_value_1 counter
+# xugudb_test_value_1 1
+# HELP xugudb_test_value_2 Same but returning always 2 as gauge.
+# TYPE xugudb_test_value_2 gauge
+# xugudb_test_value_2 2
+```
+
+
+```toml
+# 定义第一个指标配置(没有标签的例子)
+[[metric]]
+# 该指标的上下文名称,表示没有标签的情况
+context = "context_no_label"  
+# SQL 查询,返回两个固定的值:1 和 2
+request = "SELECT 1 as value_1, 2 as value_2 FROM DUAL"  
+# 对两个值的描述,分别为 1 和 2
+metricsdesc = { value_1 = "简单示例,始终返回 1。", value_2 = "同样的示例,始终返回 2。" }  
+
+#输出
+# HELP xugudb_context_no_label_value_1 简单示例,始终返回 1。
+# TYPE xugudb_context_no_label_value_1 counter
+# xugudb_context_no_label_value_1 1
+# HELP xugudb_context_no_label_value_2 同样的示例,始终返回 2。
+# TYPE xugudb_context_no_label_value_2 gauge
+# xugudb_context_no_label_value_2 2
+
+
+# 定义第二个指标配置(带标签的例子)
+[[metric]]
+# 该指标的上下文名称,表示带标签的情况
+context = "context_with_labels"  
+# 定义两个标签,"label_1" 和 "label_2",它们将用于区分指标的不同实例
+labels = [ "label_1", "label_2" ]  
+# SQL 查询,除了返回 1 和 2 之外,还返回两个标签值:'First label' 和 'Second label'
+request = "SELECT 1 as value_1, 2 as value_2, 'First label' as label_1, 'Second label' as label_2 FROM DUAL"  
+# 对两个值的描述,分别为 1 和 2
+metricsdesc = { value_1 = "简单示例,始终返回 1。", value_2 = "同样的示例,始终返回 2。" }  
+
+#输出
+# HELP xugudb_context_with_labels_value_1 简单示例,始终返回 1。
+# TYPE xugudb_context_with_labels_value_1 gauge
+# xugudb_context_with_labels_value_1{label_1="First label",label_2="Second label"} 1
+# HELP xugudb_context_with_labels_value_2 同样的示例,始终返回 2。
+# TYPE xugudb_context_with_labels_value_2 gauge
+# xugudb_context_with_labels_value_2{label_1="First label",label_2="Second label"} 2
+```
+
+```shell
+[[metric]]  # 定义一个指标配置
+context = "test_histo"  # 指标的上下文或分类,这里是 "test_histo",表示这是一个用于测试的直方图
+# SQL 查询,选择了多个字段来模拟直方图的不同桶(buckets)和相关的计数值
+request = "SELECT 'firstlabel' as label1, 'secondlabel' as label2, 3 as le_20, 19 as le_40, 31 as le_60, 40 as le_80, 45 as count, 123.45 as data FROM DUAL"
+# 描述字段 data 为直方图类型,表示数据字段的所有值的总和
+metricsdesc = { data = "Histogram - sum total of all values in the data field." }
+# 指定 data 字段的类型为直方图(histogram)
+metricstype = { data = "histogram" }
+# 定义标签字段,这里是 "label1" 和 "label2",可以作为直方图的维度来区分数据
+labels = [ "label1", "label2" ]
+# 配置每个桶(bucket)的阈值,用于定义直方图的范围
+metricsbuckets = { data = { le_20 = "20", le_40 = "40", le_60 = "60", le_80 = "80" } }
+
+  
+# 该直方图指标将生成如下形式的度量数据:
+# HELP xugudb_test_histo_data Histogram - sum total of all values in the data field.
+# TYPE xugudb_test_histo_data histogram
+# xugudb_test_histo_data_bucket{label1="firstlabel",label2="secondlabel",le="20"} 3
+# xugudb_test_histo_data_bucket{label1="firstlabel",label2="secondlabel",le="40"} 19
+# xugudb_test_histo_data_bucket{label1="firstlabel",label2="secondlabel",le="60"} 31
+# xugudb_test_histo_data_bucket{label1="firstlabel",label2="secondlabel",le="80"} 40
+# xugudb_test_histo_data_bucket{label1="firstlabel",label2="secondlabel",le="+Inf"} 45
+# xugudb_test_histo_data_sum{label1="firstlabel",label2="secondlabel"} 123.45
+# xugudb_test_histo_data_count{label1="firstlabel",label2="secondlabel"} 45
+```

+ 776 - 0
collector/collector.go

@@ -0,0 +1,776 @@
+package collector
+
+import (
+	"bytes"
+	"context"
+	"crypto/sha256"
+	"database/sql"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/BurntSushi/toml"
+	"github.com/go-kit/log"
+	"github.com/go-kit/log/level"
+	"github.com/prometheus/client_golang/prometheus"
+)
+
+// Exporter 收集 xugu 数据库的指标信息。它实现了 prometheus.Collector 接口。
+type Exporter struct {
+	config          *Config                // 配置项
+	mu              *sync.Mutex            // 互斥锁,用于保护并发访问
+	metricsToScrape Metrics                // 需要抓取的指标
+	scrapeInterval  *time.Duration         // 抓取间隔
+	dsn             string                 // 数据库连接字符串
+	duration, error prometheus.Gauge       // prometheus Gauge,用于记录抓取持续时间和错误状态
+	totalScrapes    prometheus.Counter     // prometheus Counter,记录总抓取次数
+	scrapeErrors    *prometheus.CounterVec // prometheus CounterVec,记录抓取错误的详细信息
+	scrapeResults   []prometheus.Metric    // 抓取结果存储
+	up              prometheus.Gauge       // prometheus Gauge,表示数据库是否可用
+	db              *sql.DB                // 数据库连接对象
+	logger          log.Logger             // 日志记录器
+}
+
+// Config 定义 Exporter 的配置
+type Config struct {
+	DSN                string // 数据库连接字符串
+	MaxIdleConns       int    // 最大空闲连接数
+	MaxOpenConns       int    // 最大打开连接数
+	CustomMetrics      string // 自定义指标配置
+	QueryTimeout       int    // 查询超时时间(秒)
+	DefaultMetricsFile string // 默认指标文件路径
+}
+
+// CreateDefaultConfig 返回 Exporter 的默认配置
+// 注意:返回的配置中 DSN 是空的
+func CreateDefaultConfig() *Config {
+	return &Config{
+		MaxIdleConns:       0,  // 默认最大空闲连接数为 0
+		MaxOpenConns:       10, // 默认最大打开连接数为 10
+		CustomMetrics:      "", // 默认没有自定义指标
+		QueryTimeout:       5,  // 默认查询超时时间为 5 秒
+		DefaultMetricsFile: "", // 默认没有指定指标文件
+	}
+}
+
+// Metric 描述一个指标对象
+type Metric struct {
+	Context          string                       // 指标上下文(例如查询的数据库信息)
+	Labels           []string                     // 指标标签
+	MetricsDesc      map[string]string            // 指标描述
+	MetricsType      map[string]string            // 指标类型(如 Gauge 或 Counter)
+	MetricsBuckets   map[string]map[string]string // 指标的直方图桶信息
+	FieldToAppend    string                       // 需要附加的字段
+	Request          string                       // 指标抓取的 SQL 查询
+	IgnoreZeroResult bool                         // 是否忽略结果为零的情况
+}
+
+// Metrics 是 prometheus 指标的容器结构
+type Metrics struct {
+	Metric []Metric `json:"metrics"` // 包含多个指标对象
+}
+
+var (
+	additionalMetrics Metrics                // 附加的指标
+	hashMap           = make(map[int][]byte) // 用于存储额外信息的哈希表
+	namespace         = "xugudb"             // 指标命名空间
+	exporterName      = "exporter"           // 导出器名称
+)
+
+// maskDsn 用于屏蔽数据库连接字符串中的敏感信息
+func maskDsn(dsn string) string {
+	parts := strings.Split(dsn, "@") // 将连接字符串按 "@" 分割
+	if len(parts) > 1 {
+		maskedURL := "***@" + parts[1] // 将用户名部分替换为 "***"
+		return maskedURL
+	}
+	return dsn
+}
+
+// NewExporter 创建一个新的 Exporter 实例
+func NewExporter(logger log.Logger, cfg *Config) (*Exporter, error) {
+	e := &Exporter{
+		mu:  &sync.Mutex{}, // 初始化互斥锁
+		dsn: cfg.DSN,       // 数据库连接字符串
+
+		// 定义指标:上次抓取持续时间
+		duration: prometheus.NewGauge(prometheus.GaugeOpts{
+			Namespace: namespace,
+			Subsystem: exporterName,
+			Name:      "last_scrape_duration_seconds",
+			Help:      "上次从 xugu 数据库抓取指标的持续时间(秒)。",
+		}),
+		// 定义指标:抓取总次数
+		totalScrapes: prometheus.NewCounter(prometheus.CounterOpts{
+			Namespace: namespace,
+			Subsystem: exporterName,
+			Name:      "scrapes_total",
+			Help:      "从 xugu 数据库抓取指标的总次数。",
+		}),
+		// 定义指标:抓取错误总数(分 collector 分类)
+		scrapeErrors: prometheus.NewCounterVec(prometheus.CounterOpts{
+			Namespace: namespace,
+			Subsystem: exporterName,
+			Name:      "scrape_errors_total",
+			Help:      "从 xugu 数据库抓取指标时发生错误的总次数。",
+		}, []string{"collector"}),
+		// 定义指标:上次抓取是否出错(1 表示出错,0 表示成功)
+		error: prometheus.NewGauge(prometheus.GaugeOpts{
+			Namespace: namespace,
+			Subsystem: exporterName,
+			Name:      "last_scrape_error",
+			Help:      "上次从 xugu 数据库抓取指标是否出错(1 表示出错,0 表示成功)。",
+		}),
+		// 定义指标:数据库是否可用
+		up: prometheus.NewGauge(prometheus.GaugeOpts{
+			Namespace: namespace,
+			Name:      "up",
+			Help:      "xugu 数据库服务器是否可用。",
+		}),
+		logger: logger, // 日志记录器
+		config: cfg,    // 配置信息
+	}
+
+	// 设置要抓取的默认指标
+	e.metricsToScrape = e.DefaultMetrics()
+	// 尝试连接数据库
+	err := e.connect()
+	return e, err
+}
+
+// connect 方法用于建立与 xugu 数据库的连接并进行配置。
+// 它将 DSN(Data Source Name)解析、连接数据库、配置连接池等。
+// 返回:
+// 如果连接成功,返回 nil。
+// 如果连接失败,返回相应的错误。
+func (e *Exporter) connect() error {
+	// 解析 DSN (数据源名称),检查格式是否正确
+	_, err := url.Parse(e.dsn)
+	if err != nil {
+		// 如果 DSN 格式错误,记录日志并返回错误
+		level.Error(e.logger).Log("malformed DSN: ", maskDsn(e.dsn))
+		return err
+	}
+
+	// 记录连接日志,使用 maskDsn 函数隐藏 DSN 的敏感信息
+	level.Debug(e.logger).Log("launching connection: ", maskDsn(e.dsn))
+
+	// 使用 sql.Open 打开与 xugu 数据库的连接
+	fmt.Println("连接的虚谷数据库为: ", e.dsn)
+	db, err := sql.Open("xugu", e.dsn)
+	if err != nil {
+		// 如果连接失败,记录日志并返回错误
+		level.Error(e.logger).Log("error while connecting to", e.dsn)
+		return err
+	}
+
+	// 设置最大空闲连接数,并记录日志
+	level.Debug(e.logger).Log("set max idle connections to ", e.config.MaxIdleConns)
+	db.SetMaxIdleConns(e.config.MaxIdleConns)
+
+	// 设置最大打开连接数,并记录日志
+	level.Debug(e.logger).Log("set max open connections to ", e.config.MaxOpenConns)
+	db.SetMaxOpenConns(e.config.MaxOpenConns)
+
+	// 连接成功,记录日志并将数据库连接对象赋值给 e.db
+	level.Debug(e.logger).Log("successfully connected to: ", maskDsn(e.dsn))
+	e.db = db
+
+	// 返回 nil 表示连接成功
+	return nil
+}
+
+// RunScheduledScrapes 是为想要设置定时抓取而非每次 Collect 调用时抓取的用户准备的。
+// 它会根据给定的时间间隔 (si) 定期从数据库抓取指标
+func (e *Exporter) RunScheduledScrapes(ctx context.Context, si time.Duration) {
+	// 设置抓取的时间间隔
+	e.scrapeInterval = &si
+
+	// 创建一个定时器,每隔指定的时间间隔(si)触发一次
+	ticker := time.NewTicker(si)
+
+	// 确保函数退出时定时器能够停止
+	defer ticker.Stop()
+
+	// 启动一个无限循环来定期抓取指标
+	for {
+		select {
+		// 每当定时器到达时间间隔时执行抓取操作
+		case <-ticker.C:
+			e.mu.Lock()         // 获取锁,确保不会发生并发的抓取操作
+			e.scheduledScrape() // 调用实际的抓取函数
+			e.mu.Unlock()       // 释放锁
+
+		// 如果上下文被取消(通常是服务器关闭或者停止信号),退出循环
+		case <-ctx.Done():
+			return
+		}
+	}
+}
+
+func (e *Exporter) scheduledScrape() {
+	// 创建一个用于传递指标的 channel,缓冲区大小为 5
+	metricCh := make(chan prometheus.Metric, 5)
+
+	// 创建一个 WaitGroup,确保所有指标收集完成后再返回
+	wg := &sync.WaitGroup{}
+	wg.Add(1)
+
+	// 启动一个 goroutine 来处理收集到的指标
+	go func() {
+		// 当 goroutine 完成时减少 WaitGroup 的计数
+		defer wg.Done()
+
+		// 初始化 scrapeResults 列表,用于存储所有抓取到的指标
+		e.scrapeResults = []prometheus.Metric{}
+
+		// 不断从 metricCh 获取指标并添加到 scrapeResults
+		for {
+			scrapeResult, more := <-metricCh
+			if more {
+				// 将收到的指标添加到 scrapeResults 中
+				e.scrapeResults = append(e.scrapeResults, scrapeResult)
+				continue
+			}
+			// 当 channel 被关闭时,退出循环
+			return
+		}
+	}()
+
+	// 调用 scrape 方法抓取数据库指标,将结果发送到 metricCh
+	e.scrape(metricCh)
+
+	// 报告元数据指标
+	metricCh <- e.duration           // 记录持续时间指标
+	metricCh <- e.totalScrapes       // 记录总抓取次数指标
+	metricCh <- e.error              // 记录错误指标
+	e.scrapeErrors.Collect(metricCh) // 收集并发送错误相关的指标
+
+	// 发送 up 指标,表示抓取是否成功
+	metricCh <- e.up
+
+	// 关闭指标 channel,通知 goroutine 停止处理指标
+	close(metricCh)
+
+	// 等待 goroutine 完成,确保所有指标都已处理
+	wg.Wait()
+}
+
+// Describe 描述由  数据库导出器导出的所有指标
+func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
+	// 由于无法预先知道导出器会生成哪些指标,
+	// 使用一种“简易描述方法”:执行一次 Collect 方法
+	// 并将收集到的指标描述符发送到描述通道。
+	// 问题在于需要连接  数据库,如果数据库当前不可用,
+	// 描述符将是不完整的。然而,由于这是一个独立的导出器,
+	// 而非在其他代码中作为库使用,因此最糟糕的情况是无法检测到
+	// 导出器本身创建的不一致指标。此外,受监控的  数据库 实例
+	// 发生变化可能会在运行时更改导出的指标。
+
+	metricCh := make(chan prometheus.Metric) // 用于接收指标的通道
+	doneCh := make(chan struct{})            // 用于通知完成的通道
+
+	// 启动一个 goroutine,将指标描述符发送到描述通道
+	go func() {
+		for m := range metricCh {
+			ch <- m.Desc() // 提取指标的描述符
+		}
+		close(doneCh) // 完成后关闭 doneCh
+	}()
+
+	e.Collect(metricCh) // 调用 Collect 方法收集指标
+	close(metricCh)     // 关闭指标通道
+	<-doneCh            // 等待完成通知
+}
+
+// Collect 实现了 prometheus.Collector 接口,用于收集指标数据
+func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
+	// 如果有设置抓取间隔(scrapeInterval),且抓取间隔不为 0
+	if e.scrapeInterval != nil && *e.scrapeInterval != 0 {
+		// 确保线程安全,进行只读访问
+		e.mu.Lock()
+		for _, r := range e.scrapeResults { // 将之前的抓取结果发送到指标通道
+			ch <- r
+		}
+		e.mu.Unlock()
+		return
+	}
+
+	// 如果没有抓取间隔,或者间隔为 0,则按请求进行抓取
+	e.mu.Lock()         // 加锁以确保没有并发抓取操作
+	defer e.mu.Unlock() // 解锁防止资源泄漏
+
+	// 执行抓取操作
+	e.scrape(ch)
+
+	// 发送基础指标到通道
+	ch <- e.duration           // 上次抓取持续时间
+	ch <- e.totalScrapes       // 总抓取次数
+	ch <- e.error              // 上次抓取是否出错
+	e.scrapeErrors.Collect(ch) // 抓取错误的详细统计
+	ch <- e.up                 // 数据库是否可用
+}
+
+func (e *Exporter) scrape(ch chan<- prometheus.Metric) {
+	e.totalScrapes.Inc() // 增加总抓取次数计数器
+	var err error
+	var errmutex sync.Mutex
+
+	// 延迟执行,用于记录抓取持续时间和错误状态
+	defer func(begun time.Time) {
+		e.duration.Set(time.Since(begun).Seconds()) // 设置持续时间指标
+		if err == nil {
+			e.error.Set(0) // 如果没有错误,设置错误指标为 0
+		} else {
+			e.error.Set(1) // 如果有错误,设置错误指标为 1
+		}
+	}(time.Now())
+
+	// 检查数据库连接是否正常
+	if err = e.db.Ping(); err != nil {
+		if strings.Contains(err.Error(), "sql: database is closed") {
+			level.Info(e.logger).Log("Reconnecting to DB") // 记录重新连接数据库的信息
+			err = e.connect()
+			if err != nil {
+				level.Error(e.logger).Log("error reconnecting to DB", err.Error())
+			}
+		}
+	}
+
+	// 再次检查数据库连接
+	if err = e.db.Ping(); err != nil {
+		level.Error(e.logger).Log("error pinging xugu:", err.Error())
+		e.up.Set(0) // 设置数据库不可用指标为 0
+		return
+	}
+
+	level.Debug(e.logger).Log("Successfully pinged xugu database: ", maskDsn(e.dsn))
+	e.up.Set(1) // 设置数据库可用指标为 1
+
+	// 检查指标配置是否有变化
+	if e.checkIfMetricsChanged() {
+		e.reloadMetrics() // 重新加载指标配置
+	}
+
+	wg := sync.WaitGroup{} // 定义一个 WaitGroup,用于等待所有抓取协程完成
+
+	// 遍历需要抓取的指标
+	for _, metric := range e.metricsToScrape.Metric {
+		wg.Add(1)
+		metric := metric // 为闭包创建新的局部变量(避免闭包捕获循环变量的问题)
+
+		f := func() {
+			defer wg.Done() // 在函数结束时,通知 WaitGroup 完成
+
+			// 输出调试信息,记录当前正在抓取的指标信息
+			level.Debug(e.logger).Log("About to scrape metric: ")
+			level.Debug(e.logger).Log("- Metric MetricsDesc: ", fmt.Sprintf("%+v", metric.MetricsDesc))
+			level.Debug(e.logger).Log("- Metric Context: ", metric.Context)
+			level.Debug(e.logger).Log("- Metric MetricsType: ", fmt.Sprintf("%+v", metric.MetricsType))
+			level.Debug(e.logger).Log("- Metric MetricsBuckets: ", fmt.Sprintf("%+v", metric.MetricsBuckets), "(Ignored unless Histogram type)")
+			level.Debug(e.logger).Log("- Metric Labels: ", fmt.Sprintf("%+v", metric.Labels))
+			level.Debug(e.logger).Log("- Metric FieldToAppend: ", metric.FieldToAppend)
+			level.Debug(e.logger).Log("- Metric IgnoreZeroResult: ", fmt.Sprintf("%+v", metric.IgnoreZeroResult))
+			level.Debug(e.logger).Log("- Metric Request: ", metric.Request)
+
+			// 检查请求是否为空
+			if len(metric.Request) == 0 {
+				level.Error(e.logger).Log("Error scraping for ", metric.MetricsDesc, ". Did you forget to define request in your metrics config file?")
+				return
+			}
+
+			// 检查指标描述是否为空
+			if len(metric.MetricsDesc) == 0 {
+				level.Error(e.logger).Log("Error scraping for query", metric.Request, ". Did you forget to define metricsdesc in your metrics config file?")
+				return
+			}
+
+			// 检查直方图类型的指标是否定义了桶(Buckets)
+			for column, metricType := range metric.MetricsType {
+				if metricType == "histogram" {
+					_, ok := metric.MetricsBuckets[column]
+					if !ok {
+						level.Error(e.logger).Log("Unable to find MetricsBuckets configuration key for metric. (metric=" + column + ")")
+						return
+					}
+				}
+			}
+
+			scrapeStart := time.Now() // 记录抓取开始时间
+			// 执行实际的指标抓取操作
+			if err1 := e.ScrapeMetric(e.db, ch, metric); err1 != nil {
+				// 如果抓取过程中发生错误,记录错误信息并更新错误计数器
+				errmutex.Lock()
+				{
+					err = err1
+				}
+				errmutex.Unlock()
+				level.Error(e.logger).Log("scrapeMetricContext", metric.Context, "ScrapeDuration", time.Since(scrapeStart), "msg", err1.Error())
+				e.scrapeErrors.WithLabelValues(metric.Context).Inc()
+			} else {
+				// 如果抓取成功,记录成功信息
+				level.Debug(e.logger).Log("successfully scraped metric: ", metric.Context, metric.MetricsDesc, time.Since(scrapeStart))
+			}
+		}
+
+		go f() // 启动一个协程执行抓取函数
+	}
+	wg.Wait() // 等待所有抓取协程完成
+}
+
+// ScrapeMetric 是接口方法,调用 scrapeGenericValues 使用 Metric 结构体的值来抓取指标
+func (e *Exporter) ScrapeMetric(db *sql.DB, ch chan<- prometheus.Metric, metricDefinition Metric) error {
+	level.Debug(e.logger).Log("调用函数 ScrapeGenericValues()")
+	// 调用 scrapeGenericValues 函数来抓取指标
+	return e.scrapeGenericValues(db, ch, metricDefinition.Context, metricDefinition.Labels,
+		metricDefinition.MetricsDesc, metricDefinition.MetricsType, metricDefinition.MetricsBuckets,
+		metricDefinition.FieldToAppend, metricDefinition.IgnoreZeroResult,
+		metricDefinition.Request)
+}
+
+// scrapeGenericValues 是一个通用的抓取指标的方法
+// 参数:
+// - db: 数据库连接对象,用于执行 SQL 查询。
+// - ch: 用于发送抓取到的 Prometheus 指标的通道。
+// - context: 指标的上下文,用于标识指标的来源或类型。
+// - labels: 指标的标签,用于给指标添加上下文信息。
+// - metricsDesc: 存储指标名称及其帮助信息的映射。
+// - metricsType: 存储指标类型(例如 gauge 或 histogram)的映射。
+// - metricsBuckets: 存储直方图类型指标的桶(buckets)配置。
+// - fieldToAppend: 用于动态附加到指标名称中的字段。
+// - ignoreZeroResult: 如果为 true,则忽略没有结果的指标,否则会返回错误。
+// - request: 执行数据库查询的 SQL 请求。
+func (e *Exporter) scrapeGenericValues(db *sql.DB, ch chan<- prometheus.Metric, context string, labels []string,
+	metricsDesc map[string]string, metricsType map[string]string, metricsBuckets map[string]map[string]string, fieldToAppend string, ignoreZeroResult bool, request string) error {
+
+	metricsCount := 0 // 记录抓取的指标数量
+
+	// 通用解析器,用于处理查询结果行
+	genericParser := func(row map[string]string) error {
+		labelsValues := []string{} // 存储标签的值
+		// 遍历标签数组,将对应标签的值添加到 labelsValues 中
+		for _, label := range labels {
+			labelsValues = append(labelsValues, row[strings.ToUpper(label)])
+		}
+
+		// 遍历所有指标,抓取其对应的值
+		for metric, metricHelp := range metricsDesc {
+			// 将指标值转换为浮动点数
+
+			value, err := strconv.ParseFloat(strings.TrimSpace(row[strings.ToUpper(metric)]), 64)
+			if err != nil {
+				// 如果无法转换为浮动点数,跳过该指标
+				level.Error(e.logger).Log("msg", "无法将当前值转换为浮动点数", "metric", metric, "metricHelp", metricHelp, "value", row[metric])
+				continue
+			}
+
+			level.Debug(e.logger).Log("查询结果:", value)
+
+			// 如果指标名称不使用附加字段(fieldToAppend),则使用普通的描述
+			if strings.Compare(fieldToAppend, "") == 0 {
+				desc := prometheus.NewDesc(
+					prometheus.BuildFQName(namespace, context, metric), // 构建 Prometheus 描述符
+					metricHelp, // 指标的帮助信息
+					labels,     // 指标的标签
+					nil,        // 没有附加标签
+				)
+				// 如果该指标是直方图类型
+				if metricsType[strings.ToLower(metric)] == "histogram" {
+					// 获取直方图的计数
+
+					count, err := strconv.ParseUint(strings.TrimSpace(row["COUNT"]), 10, 64)
+
+					if err != nil {
+						// 如果无法转换计数值为整型,跳过该指标
+						level.Error(e.logger).Log("无法将 count 值转换为整型 (metric=" + metric +
+							",metricHelp=" + metricHelp + ",value=<" + row["COUNT"] + ">)")
+						continue
+					}
+					// 创建桶(buckets)并填充数据
+					buckets := make(map[float64]uint64)
+					for field, le := range metricsBuckets[strings.ToUpper(metric)] {
+						lelimit, err := strconv.ParseFloat(strings.TrimSpace(le), 64)
+						if err != nil {
+							// 如果桶的限制值无法转换为浮动点数,跳过
+							level.Error(e.logger).Log("无法将桶的限制值转换为浮动点数 (metric=" + metric +
+								",metricHelp=" + metricHelp + ",bucketlimit=<" + le + ">)")
+							continue
+						}
+						fmt.Println("row[field]", row[field])
+						counter, err := strconv.ParseUint(strings.TrimSpace(row[strings.ToUpper(field)]), 10, 64)
+						if err != nil {
+							// 如果桶的计数无法转换为整型,跳过
+							level.Error(e.logger).Log("无法将桶计数值转换为整型 (metric=" + metric +
+								",metricHelp=" + metricHelp + ",value=<" + row[field] + ">)")
+							continue
+						}
+						buckets[lelimit] = counter
+					}
+					// 将直方图指标发送到 Prometheus 通道
+					ch <- prometheus.MustNewConstHistogram(desc, count, value, buckets, labelsValues...)
+				} else {
+					// 对于非直方图类型指标,直接将其发送到 Prometheus 通道
+					ch <- prometheus.MustNewConstMetric(desc, getMetricType(metric, metricsType), value, labelsValues...)
+				}
+			} else {
+				// 如果指标使用附加字段,则使用附加字段值构建描述符
+				desc := prometheus.NewDesc(
+					prometheus.BuildFQName(namespace, context, cleanName(row[fieldToAppend])),
+					metricHelp,
+					nil, nil, // 没有标签
+				)
+				// 处理直方图类型指标
+				if metricsType[strings.ToLower(metric)] == "histogram" {
+					// 获取直方图的计数
+					count, err := strconv.ParseUint(strings.TrimSpace(row["count"]), 10, 64)
+					if err != nil {
+						// 如果无法转换计数值为整型,跳过该指标
+						level.Error(e.logger).Log("无法将 count 值转换为整型 (metric=" + metric +
+							",metricHelp=" + metricHelp + ",value=<" + row["count"] + ">)")
+						continue
+					}
+					// 创建桶(buckets)并填充数据
+					buckets := make(map[float64]uint64)
+					for field, le := range metricsBuckets[strings.ToUpper(metric)] {
+						lelimit, err := strconv.ParseFloat(strings.TrimSpace(le), 64)
+						if err != nil {
+							// 如果桶的限制值无法转换为浮动点数,跳过
+							level.Error(e.logger).Log("无法将桶的限制值转换为浮动点数 (metric=" + metric +
+								",metricHelp=" + metricHelp + ",bucketlimit=<" + le + ">)")
+							continue
+						}
+						counter, err := strconv.ParseUint(strings.TrimSpace(row[field]), 10, 64)
+						if err != nil {
+							// 如果桶的计数无法转换为整型,跳过
+							level.Error(e.logger).Log("无法将桶计数值转换为整型 (metric=" + metric +
+								",metricHelp=" + metricHelp + ",value=<" + row[field] + ">)")
+							continue
+						}
+						buckets[lelimit] = counter
+					}
+					// 将直方图指标发送到 Prometheus 通道
+					ch <- prometheus.MustNewConstHistogram(desc, count, value, buckets)
+				} else {
+					// 对于非直方图类型指标,直接将其发送到 Prometheus 通道
+					ch <- prometheus.MustNewConstMetric(desc, getMetricType(metric, metricsType), value)
+				}
+			}
+			metricsCount++ // 增加抓取的指标计数
+		}
+		return nil
+	}
+
+	level.Debug(e.logger).Log("调用函数 GeneratePrometheusMetrics()")
+	// 调用函数生成 Prometheus 指标
+	err := e.generatePrometheusMetrics(db, genericParser, request)
+	level.Debug(e.logger).Log("ScrapeGenericValues() - metricsCount: ", metricsCount)
+	if err != nil {
+		return err
+	}
+
+	// 如果没有找到任何指标且未忽略零结果,则返回错误
+	if !ignoreZeroResult && metricsCount == 0 {
+		return errors.New("没有找到任何指标")
+	}
+	return err
+}
+
+// generatePrometheusMetrics 从数据库中执行 SQL 查询,解析查询结果并调用解析函数处理每一行数据。
+// 参数:
+// db: 需要查询的数据库连接。
+// parse: 一个处理每一行结果的函数,它接收一个映射(map)作为输入参数。
+// query: 要执行的 SQL 查询。
+// 返回:
+// 如果查询或处理过程中出现错误,返回错误。
+func (e *Exporter) generatePrometheusMetrics(db *sql.DB, parse func(row map[string]string) error, query string) error {
+	// 设置查询超时,避免长时间阻塞
+	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(e.config.QueryTimeout)*time.Second)
+	defer cancel()
+
+	// 执行 SQL 查询
+	rows, err := db.QueryContext(ctx, query)
+
+	// 检查是否超时
+	if errors.Is(ctx.Err(), context.DeadlineExceeded) {
+		return errors.New("xugu query timed out")
+	}
+
+	// 检查查询是否出错
+	if err != nil {
+		return err
+	}
+
+	// 获取查询结果的列名
+	cols, err := rows.Columns()
+	defer rows.Close()
+
+	// 如果查询结果的列名获取失败,返回错误
+	if err != nil {
+		return err
+	}
+
+	// 遍历查询结果的每一行
+	for rows.Next() {
+		// 创建一个切片,用来表示每一列的值,以及一个切片,用来存储每列的指针
+		columns := make([]interface{}, len(cols))
+		columnPointers := make([]interface{}, len(cols))
+		for i := range columns {
+			columnPointers[i] = &columns[i]
+		}
+
+		// 将查询结果扫描到 columnPointers 切片中
+		if err := rows.Scan(columnPointers...); err != nil {
+			return err
+		}
+
+		// 创建一个 map,用来存储每列的值,列名作为 key
+		m := make(map[string]string)
+		for i, colName := range cols {
+			// 获取列的值并将其格式化为字符串,存入 map 中
+			val := columnPointers[i].(*interface{})
+			//m[strings.ToLower(colName)] = fmt.Sprintf("%v", *val)
+			m[strings.ToUpper(colName)] = fmt.Sprintf("%v", *val)
+
+		}
+		fmt.Println("m:", m)
+		// 调用传入的解析函数处理每一行数据
+		if err := parse(m); err != nil {
+			return err
+		}
+	}
+	// 查询和处理成功后返回 nil
+	return nil
+}
+
+// getMetricType 函数根据传入的 metricType 和 metricsType 获取相应的 Prometheus 值类型 (prometheus.ValueType)
+// 参数:
+// metricType: 一个字符串,表示传入的指标类型
+// metricsType: 一个字典映射,包含了每个 metricType 对应的字符串类型
+// 返回:
+// 返回对应的 prometheus.ValueType
+func getMetricType(metricType string, metricsType map[string]string) prometheus.ValueType {
+	// 定义一个映射,将字符串类型映射到 Prometheus 的 ValueType 类型
+	var strToPromType = map[string]prometheus.ValueType{
+		"gauge":     prometheus.GaugeValue,   // "gauge" 映射为 Prometheus 的 GaugeValue 类型
+		"counter":   prometheus.CounterValue, // "counter" 映射为 Prometheus 的 CounterValue 类型
+		"histogram": prometheus.UntypedValue, // "histogram" 映射为 Prometheus 的 UntypedValue 类型
+	}
+
+	// 将传入的 metricType 转为小写并在 metricsType 字典中查找其对应的类型
+	strType, ok := metricsType[strings.ToLower(metricType)]
+	if !ok {
+		// 如果在 metricsType 中没有找到对应的类型,则默认返回 prometheus.GaugeValue
+		return prometheus.GaugeValue
+	}
+
+	// 查找对应的 Prometheus ValueType 类型
+	valueType, ok := strToPromType[strings.ToLower(strType)]
+	if !ok {
+		// 如果在 strToPromType 中没有找到对应的 Prometheus 类型,则抛出 panic
+		panic(errors.New("Error while getting prometheus type " + strings.ToLower(strType)))
+	}
+
+	// 返回对应的 Prometheus 类型
+	return valueType
+}
+
+// checkIfMetricsChanged 方法检查配置文件中定义的自定义指标文件是否发生变化。
+// 如果文件发生了变化(通过计算哈希值进行比较),则返回 true,表示需要重新加载指标。
+// 返回:
+// 如果有文件发生变化,则返回 true,否则返回 false。
+func (e *Exporter) checkIfMetricsChanged() bool {
+	// 遍历自定义指标文件的列表,分隔符为逗号
+	for i, _customMetrics := range strings.Split(e.config.CustomMetrics, ",") {
+		// 如果自定义指标文件名为空,跳过当前循环
+		if len(_customMetrics) == 0 {
+			continue
+		}
+
+		// 记录正在检查的文件
+		level.Debug(e.logger).Log("checking modifications in following metrics definition file:", _customMetrics)
+
+		// 创建一个新的 SHA256 哈希计算器
+		h := sha256.New()
+
+		// 使用 hashFile 函数计算文件的哈希值
+		if err := hashFile(h, _customMetrics); err != nil {
+			// 如果文件哈希计算失败,记录错误并返回 false
+			level.Error(e.logger).Log("unable to get file hash", err.Error())
+			return false
+		}
+
+		// 如果当前文件的哈希值与之前存储的哈希值不同,表示文件已更改
+		if !bytes.Equal(hashMap[i], h.Sum(nil)) {
+			// 记录文件已更改,触发重新加载指标
+			level.Info(e.logger).Log(_customMetrics, "has been changed. Reloading metrics...")
+			// 更新存储的哈希值
+			hashMap[i] = h.Sum(nil)
+			return true
+		}
+	}
+
+	// 如果所有文件都没有变化,返回 false
+	return false
+}
+
+// reloadMetrics 方法用于重新加载 Prometheus 指标。
+// 它首先清空当前的指标列表,加载默认的指标,然后根据配置加载自定义的指标。
+// 如果有自定义指标文件,则加载并解析这些文件,最终将所有指标添加到 e.metricsToScrape 中。
+// 返回值:
+// 无,直接操作实例的状态(e.metricsToScrape)。
+func (e *Exporter) reloadMetrics() {
+	// 清空当前的指标列表
+	e.metricsToScrape.Metric = []Metric{}
+
+	// 加载默认指标
+	defaultMetrics := e.DefaultMetrics()
+	e.metricsToScrape.Metric = defaultMetrics.Metric
+
+	// 如果有自定义指标配置,则加载它们
+	if strings.Compare(e.config.CustomMetrics, "") != 0 {
+		// 遍历自定义指标配置文件列表(由逗号分隔)
+		for _, _customMetrics := range strings.Split(e.config.CustomMetrics, ",") {
+			// 判断是 TOML 文件还是 YAML 文件,根据文件后缀调用不同的加载函数
+			if strings.HasSuffix(_customMetrics, "toml") {
+				// 加载 TOML 配置文件
+				if err := loadTomlMetricsConfig(_customMetrics, &additionalMetrics); err != nil {
+					// 如果加载失败,触发 panic
+					panic(err)
+				}
+			}
+
+			// 记录成功加载自定义指标
+			level.Info(e.logger).Log("event", "Successfully loaded custom metrics from "+_customMetrics)
+			// 输出调试信息,显示解析后的自定义指标内容
+			level.Debug(e.logger).Log("custom metrics parsed content", fmt.Sprintf("%+v", additionalMetrics))
+
+			// 将加载的自定义指标添加到 e.metricsToScrape.Metric 中
+			e.metricsToScrape.Metric = append(e.metricsToScrape.Metric, additionalMetrics.Metric...)
+		}
+	} else {
+		// 如果没有定义自定义指标,记录调试日志
+		level.Debug(e.logger).Log("No custom metrics defined.")
+	}
+}
+
+// loadTomlMetricsConfig 函数用于加载并解析 TOML 配置文件。
+// 参数:
+// _customMetrics: 自定义指标文件的路径。
+// metrics: 用于存储解析后的自定义指标的结构体。
+// 返回:
+// 如果解析成功,返回 nil;如果出现错误,返回相应的错误。
+func loadTomlMetricsConfig(_customMetrics string, metrics *Metrics) error {
+	// 使用 toml.DecodeFile 解析 TOML 配置文件
+	if _, err := toml.DecodeFile(_customMetrics, metrics); err != nil {
+		// 如果解析失败,返回格式化的错误消息
+		return fmt.Errorf("cannot read the metrics config %s: %w", _customMetrics, err)
+	}
+
+	// 解析成功,返回 nil
+	return nil
+}

+ 48 - 0
collector/default_metrics.go

@@ -0,0 +1,48 @@
+package collector
+
+import (
+	"errors"
+	"strings"
+
+	"github.com/BurntSushi/toml"
+	"github.com/go-kit/log/level"
+)
+
+// 定义一个包含默认指标配置的常量字符串
+const defaultMetricsConst = `
+
+`
+
+// DefaultMetrics 是一种加载默认指标的实现方式(稍显临时的方式)
+func (e *Exporter) DefaultMetrics() Metrics {
+	var metricsToScrape Metrics // 用于存储需要抓取的指标
+	var err error
+
+	// 如果配置中指定了默认指标文件
+	if e.config.DefaultMetricsFile != "" {
+		// 根据文件后缀选择解析方法
+		if strings.HasSuffix(e.config.DefaultMetricsFile, "toml") {
+			// 加载 TOML 格式的指标配置
+			err = loadTomlMetricsConfig(e.config.DefaultMetricsFile, &metricsToScrape)
+		}
+
+		// 如果加载成功,返回加载的指标
+		if err == nil {
+			return metricsToScrape
+		}
+
+		// 如果加载失败,记录错误并警告将使用默认指标
+		level.Error(e.logger).Log("没有找到默认指标文件 defaultMetricsFile", e.config.DefaultMetricsFile, "msg", err)
+		//level.Warn(e.logger).Log("msg", "将使用默认指标继续运行")
+	}
+
+	// 如果没有指定文件或加载失败,则从常量中加载默认指标
+	if _, err := toml.Decode(defaultMetricsConst, &metricsToScrape); err != nil {
+		// 如果加载失败,记录错误并触发 panic
+		level.Error(e.logger).Log("msg", err.Error())
+		panic(errors.New("加载默认指标时发生错误:" + defaultMetricsConst))
+	}
+
+	// 返回加载的默认指标
+	return metricsToScrape
+}

+ 65 - 0
collector/utils.go

@@ -0,0 +1,65 @@
+package collector
+
+import (
+	"hash"
+	"io"
+	"os"
+	"strings"
+)
+
+// -----------------------------------------------------------------------------------
+// cleanName 函数清理输入的字符串,通过替换或删除特定字符来规范化它。
+// 参数:
+// s: 需要处理的输入字符串
+// 返回:
+// 返回经过清理和规范化后的字符串
+func cleanName(s string) string {
+	// 将字符串中的所有空格替换为下划线
+	s = strings.ReplaceAll(s, " ", "_") // Remove spaces
+
+	// 将字符串中的所有连字符(-)替换为下划线
+	s = strings.ReplaceAll(s, "-", "_") // Remove hyphens
+
+	// 将字符串中的所有左括号(()移除
+	s = strings.ReplaceAll(s, "(", "") // Remove open parenthesis
+
+	// 将字符串中的所有右括号())移除
+	s = strings.ReplaceAll(s, ")", "") // Remove close parenthesis
+
+	// 将字符串中的所有正斜杠(/)移除
+	s = strings.ReplaceAll(s, "/", "") // Remove forward slashes
+
+	// 将字符串中的所有星号(*)移除
+	s = strings.ReplaceAll(s, "*", "") // Remove asterisks
+
+	// 将整个字符串转换为小写字母
+	s = strings.ToLower(s)
+
+	// 返回处理后的字符串
+	return s
+}
+
+// hashFile 函数用于计算指定文件的哈希值。
+// 参数:
+// h: 一个实现了 hash.Hash 接口的哈希对象(如 sha256.New()),用于存储文件的哈希值。
+// fn: 要计算哈希值的文件路径。
+// 返回:
+// 如果计算过程中出现错误,返回该错误;否则返回 nil。
+func hashFile(h hash.Hash, fn string) error {
+	// 打开指定的文件
+	f, err := os.Open(fn)
+	if err != nil {
+		// 如果文件打开失败,返回错误
+		return err
+	}
+	defer f.Close() // 确保函数结束时关闭文件
+
+	// 将文件内容复制到哈希计算器 h 中
+	if _, err := io.Copy(h, f); err != nil {
+		// 如果复制过程中发生错误,返回错误
+		return err
+	}
+
+	// 返回 nil,表示没有发生错误
+	return nil
+}

+ 38 - 0
go.mod

@@ -0,0 +1,38 @@
+module xugu_exporter
+
+go 1.22.2
+
+require (
+	gitee.com/XuguDB/go-xugu-driver v1.0.5
+	github.com/BurntSushi/toml v1.4.0
+	github.com/alecthomas/kingpin/v2 v2.4.0
+	github.com/go-kit/log v0.2.1
+	github.com/prometheus/client_golang v1.20.5
+	github.com/prometheus/common v0.61.0
+	github.com/prometheus/exporter-toolkit v0.13.2
+)
+
+require (
+	github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
+	github.com/go-logfmt/logfmt v0.5.1 // indirect
+	github.com/jpillora/backoff v1.0.0 // indirect
+	github.com/klauspost/compress v1.17.9 // indirect
+	github.com/mdlayher/socket v0.4.1 // indirect
+	github.com/mdlayher/vsock v1.2.1 // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
+	github.com/prometheus/client_model v0.6.1 // indirect
+	github.com/prometheus/procfs v0.15.1 // indirect
+	github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
+	golang.org/x/crypto v0.31.0 // indirect
+	golang.org/x/net v0.32.0 // indirect
+	golang.org/x/oauth2 v0.24.0 // indirect
+	golang.org/x/sync v0.10.0 // indirect
+	golang.org/x/sys v0.28.0 // indirect
+	golang.org/x/text v0.21.0 // indirect
+	google.golang.org/protobuf v1.35.2 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+)

+ 86 - 0
go.sum

@@ -0,0 +1,86 @@
+gitee.com/XuguDB/go-xugu-driver v1.0.3 h1:sHEfQJqbTPkS4wLAU+WvE12ba1P1DvXQxtwQB+zcC2A=
+gitee.com/XuguDB/go-xugu-driver v1.0.3/go.mod h1:VJlSyp+6BjYL1US00cVb4traVISlm8iGywYe3viLPbE=
+gitee.com/XuguDB/go-xugu-driver v1.0.5 h1:TfckCTgq7n+ekui1F4+b0zN0hcTKi19QhQKM5xaOYiI=
+gitee.com/XuguDB/go-xugu-driver v1.0.5/go.mod h1:VJlSyp+6BjYL1US00cVb4traVISlm8iGywYe3viLPbE=
+github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
+github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
+github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
+github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
+github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
+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/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+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/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
+github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
+github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
+github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+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/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
+github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
+github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ=
+github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE=
+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/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+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.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
+github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
+github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
+github.com/prometheus/exporter-toolkit v0.13.2 h1:Z02fYtbqTMy2i/f+xZ+UK5jy/bl1Ex3ndzh06T/Q9DQ=
+github.com/prometheus/exporter-toolkit v0.13.2/go.mod h1:tCqnfx21q6qN1KA4U3Bfb8uWzXfijIrJz3/kTIqMV7g=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
+github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
+golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
+golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
+golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
+golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
+google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 196 - 0
main.go

@@ -0,0 +1,196 @@
+package main
+
+import (
+	"context"
+	"log/slog"
+	"net/http"
+	"os"
+
+	"xugu_exporter/collector"
+
+	_ "gitee.com/XuguDB/go-xugu-driver"
+	kingpin "github.com/alecthomas/kingpin/v2"
+	"github.com/go-kit/log/level"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/collectors"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
+	"github.com/prometheus/common/promlog"
+	"github.com/prometheus/common/promlog/flag"
+	"github.com/prometheus/common/version"
+	"github.com/prometheus/exporter-toolkit/web"
+	webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag"
+	// Required for debugging
+	// _ "net/http/pprof"
+)
+
+var (
+	// Version will be set at build time.
+	Version = "0.0.1.dev" // 版本号,通常在构建时设置。这里初始化为一个开发版本号。
+
+	// 使用 Kingpin 库定义命令行标志,并为每个标志提供帮助信息和默认值。
+
+	// metricPath 定义了暴露指标的路径。默认值来自环境变量 "TELEMETRY_PATH",若未设置则默认为 "/metrics"。
+	metricPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics. (env: TELEMETRY_PATH)").Default(getEnv("TELEMETRY_PATH", "/metrics")).String()
+
+	// dsn 定义了数据库的连接字符串。默认值来自环境变量 "DATA_SOURCE_NAME",若未设置则默认为空字符串。
+	dsn = kingpin.Flag("database.dsn",
+		"Connection string to a data source. (env: DATA_SOURCE_NAME)",
+	).Default(getEnv("DATA_SOURCE_NAME", "")).String()
+
+	// dsnFile 定义了从文件读取数据库连接字符串的路径。默认值来自环境变量 "DATA_SOURCE_NAME_FILE",若未设置则默认为空字符串。
+	dsnFile = kingpin.Flag("database.dsnFile",
+		"File to read a string to a data source from. (env: DATA_SOURCE_NAME_FILE)",
+	).Default(getEnv("DATA_SOURCE_NAME_FILE", "")).String()
+
+	// defaultFileMetrics 定义了存储默认指标的文件路径。该文件支持 TOML 或 YAML 格式。默认值来自环境变量 "DEFAULT_METRICS",若未设置则默认为 "default-metrics.toml"。
+	defaultFileMetrics = kingpin.Flag(
+		"default.metrics",
+		"File with default metrics in a toml  format. (env: DEFAULT_METRICS)",
+	).Default(getEnv("DEFAULT_METRICS", "default-metrics.toml")).String()
+
+	// customMetrics 定义了包含自定义指标的文件路径。该文件支持 TOML 或 YAML 格式。默认值来自环境变量 "CUSTOM_METRICS",若未设置则默认为空字符串。
+	customMetrics = kingpin.Flag(
+		"custom.metrics",
+		"File that may contain various custom metrics in a toml format. (env: CUSTOM_METRICS)",
+	).Default(getEnv("CUSTOM_METRICS", "")).String()
+
+	// queryTimeout 定义了查询的超时时间,单位为秒。默认值来自环境变量 "QUERY_TIMEOUT",若未设置则默认为 5 秒。
+	queryTimeout = kingpin.Flag(
+		"query.timeout",
+		"Query timeout (in seconds). (env: QUERY_TIMEOUT)",
+	).Default(getEnv("QUERY_TIMEOUT", "5")).Int()
+
+	// maxIdleConns 定义了连接池中最大空闲连接数。默认值来自环境变量 "DATABASE_MAXIDLECONNS",若未设置则默认为 0。
+	maxIdleConns = kingpin.Flag(
+		"database.maxIdleConns",
+		"Number of maximum idle connections in the connection pool. (env: DATABASE_MAXIDLECONNS)",
+	).Default(getEnv("DATABASE_MAXIDLECONNS", "0")).Int()
+
+	// maxOpenConns 定义了连接池中最大打开连接数。默认值来自环境变量 "DATABASE_MAXOPENCONNS",若未设置则默认为 10。
+	maxOpenConns = kingpin.Flag(
+		"database.maxOpenConns",
+		"Number of maximum open connections in the connection pool. (env: DATABASE_MAXOPENCONNS)",
+	).Default(getEnv("DATABASE_MAXOPENCONNS", "10")).Int()
+
+	// scrapeInterval 定义了每次抓取数据的间隔时间。默认值为 0,表示在每次收集请求时进行抓取。
+	scrapeInterval = kingpin.Flag(
+		"scrape.interval",
+		"Interval between each scrape. Default is to scrape on collect requests",
+	).Default("0s").Duration()
+
+	// toolkitFlags 是与 Web 相关的附加标志,添加到 `kingpin.CommandLine` 上,默认端口为 ":9161"。
+	toolkitFlags = webflag.AddFlags(kingpin.CommandLine, ":9161")
+)
+
+func main() {
+	// 创建 Prometheus 日志配置对象
+	promLogConfig := &promlog.Config{}
+
+	// 添加命令行标志以配置 Prometheus 日志
+	flag.AddFlags(kingpin.CommandLine, promLogConfig)
+	// 设置帮助标志的快捷键
+	kingpin.HelpFlag.Short('\n')
+	// 设置程序版本信息
+	kingpin.Version(version.Print("xugudb_exporter"))
+	// 解析命令行输入的参数
+	kingpin.Parse()
+	// 创建新的日志记录器
+	logger := promlog.New(promLogConfig)
+
+	// 如果提供了 dsnFile 参数,读取文件中的数据库连接信息
+	if dsnFile != nil && *dsnFile != "" {
+		dsnFileContent, err := os.ReadFile(*dsnFile)
+		if err != nil {
+			// 如果文件读取失败,记录错误并退出程序
+			level.Error(logger).Log("msg", "Unable to read DATA_SOURCE_NAME_FILE", "file", dsnFile, "error", err)
+			os.Exit(1)
+		}
+		// 将文件内容作为数据库连接字符串
+		*dsn = string(dsnFileContent)
+	}
+
+	// 初始化配置对象,将从命令行解析的参数赋值给配置
+	config := &collector.Config{
+		DSN:                *dsn,                // 数据库连接字符串
+		MaxOpenConns:       *maxOpenConns,       // 最大打开连接数
+		MaxIdleConns:       *maxIdleConns,       // 最大空闲连接数
+		CustomMetrics:      *customMetrics,      // 自定义指标文件路径
+		QueryTimeout:       *queryTimeout,       // 查询超时时间
+		DefaultMetricsFile: *defaultFileMetrics, // 默认指标文件路径
+	}
+
+	// 创建一个新的 Exporter 实例,用于收集 xugu 数据库的指标
+	exporter, err := collector.NewExporter(logger, config)
+	if err != nil {
+		// 如果创建 Exporter 失败,记录错误并退出程序
+		level.Error(logger).Log("unable to connect to DB", err)
+	}
+
+	// 如果设置了抓取间隔(scrapeInterval),则启动定时抓取指标的任务
+	if *scrapeInterval != 0 {
+		ctx, cancel := context.WithCancel(context.Background())
+		defer cancel()
+		// 启动一个 goroutine,定期从数据库抓取指标
+		go exporter.RunScheduledScrapes(ctx, *scrapeInterval)
+	}
+
+	// 将 Exporter 注册到 Prometheus 中,供 Prometheus 进行指标抓取
+	prometheus.MustRegister(exporter)
+	// 注册构建信息收集器,用于报告程序的版本信息等元数据
+	prometheus.MustRegister(collectors.NewBuildInfoCollector())
+
+	// 记录启动信息
+	level.Info(logger).Log("msg", "Starting xugudb_exporter", "version", version.Info())
+	level.Info(logger).Log("msg", "Build context", "build", version.BuildContext())
+	level.Info(logger).Log("msg", "Collect from: ", "metricPath", *metricPath)
+
+	// 设置 Prometheus HTTP handler 的选项
+	opts := promhttp.HandlerOpts{
+		ErrorHandling: promhttp.ContinueOnError, // 遇到错误时继续处理其他请求
+	}
+	// 配置 /metrics 路径用于暴露 Prometheus 格式的指标
+	http.Handle(*metricPath, promhttp.HandlerFor(prometheus.DefaultGatherer, opts))
+
+	// 配置根路径(/),返回一个简单的 HTML 页面,包含版本信息和指标链接
+	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte("<html><head><title>xugu DB Exporter " + Version + "</title></head><body><h1>xugu DB Exporter " + Version + "</h1><p><a href='" + *metricPath + "'>Metrics</a></p></body></html>"))
+	})
+
+	// 创建一个日志处理器,输出到标准输出(os.Stdout)
+	handlerS := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
+		Level: slog.LevelInfo, // 设置最低日志级别为 Info
+	})
+
+	// 创建一个新的 logger
+	loggerS := slog.New(handlerS)
+	// 创建一个新的 logger
+	//loggerS := slog.New(slog.NewTextHandler(slog.Default()))
+
+	// 使用 logger 记录日志
+	loggerS.Info("This is an info message")
+	loggerS.Warn("This is a warning message")
+	loggerS.Error("This is an error message")
+
+	// 如果需要,可以添加更多的上下文信息
+	loggerS.With("context", "example").Info("This is a context-aware info message")
+	// 创建一个 HTTP 服务器实例
+	server := &http.Server{}
+	// 启动 HTTP 服务器,监听并提供 Prometheus 指标
+	if err := web.ListenAndServe(server, toolkitFlags, loggerS); err != nil {
+		// 如果服务器启动失败,记录错误并退出程序
+		level.Error(logger).Log("msg", "Listening error", "reason", err)
+		os.Exit(1)
+	}
+}
+
+// getEnv 返回指定环境变量的值,如果环境变量未设置,则返回提供的备用值。
+func getEnv(key, fallback string) string {
+	// os.LookupEnv 用于查找环境变量的值。如果环境变量存在,返回值和一个布尔值(true/false);
+	// 如果环境变量不存在,返回空字符串和 false。
+	if value, ok := os.LookupEnv(key); ok {
+		// 如果环境变量存在,返回其值
+		return value
+	}
+	// 如果环境变量不存在,返回备用值
+	return fallback
+}

+ 10 - 0
metric-dual-example.toml

@@ -0,0 +1,10 @@
+# 定义第二个指标配置(带标签的例子)
+[[metric]]
+# 该指标的上下文名称,表示带标签的情况
+context = "context_with_labels" 
+# 定义两个标签,"label_1" 和 "label_2",它们将用于区分指标的不同实例
+labels = [ "label_1", "label_2" ] 
+# SQL 查询,除了返回 1 和 2 之外,还返回两个标签值:'First label' 和 'Second label'
+request = "SELECT 1 as value_1, 2 as value_2, 'First label' as label_1, 'Second label' as label_2 FROM DUAL" 
+# 对两个值的描述,分别为 1 和 2
+metricsdesc = { value_1 = "简单示例,始终返回 1。", value_2 = "同样的示例,始终返回 2。" } 

+ 24 - 0
metric-histogram-example.toml

@@ -0,0 +1,24 @@
+[[metric]]  # 定义一个指标配置
+context = "test_histo"  # 指标的上下文或分类,这里是 "test_histo",表示这是一个用于测试的直方图
+# SQL 查询,选择了多个字段来模拟直方图的不同桶(buckets)和相关的计数值
+request = "SELECT 'firstlabel' as label1, 'secondlabel' as label2, 3 as le_20, 19 as le_40, 31 as le_60, 40 as le_80, 45 as count, 123.45 as data FROM DUAL"
+# 描述字段 data 为直方图类型,表示数据字段的所有值的总和
+metricsdesc = { data = "Histogram - sum total of all values in the data field." }
+# 指定 data 字段的类型为直方图(histogram)
+metricstype = { data = "histogram" }
+# 定义标签字段,这里是 "label1" 和 "label2",可以作为直方图的维度来区分数据
+labels = [ "label1", "label2" ]
+# 配置每个桶(bucket)的阈值,用于定义直方图的范围
+metricsbuckets = { data = { le_20 = "20", le_40 = "40", le_60 = "60", le_80 = "80" } }
+
+
+# 该直方图指标将生成如下形式的度量数据:
+# HELP xugudb_test_histo_data Histogram - sum total of all values in the data field.
+# TYPE xugudb_test_histo_data histogram
+# xugudb_test_histo_data_bucket{label1="firstlabel",label2="secondlabel",le="20"} 3
+# xugudb_test_histo_data_bucket{label1="firstlabel",label2="secondlabel",le="40"} 19
+# xugudb_test_histo_data_bucket{label1="firstlabel",label2="secondlabel",le="60"} 31
+# xugudb_test_histo_data_bucket{label1="firstlabel",label2="secondlabel",le="80"} 40
+# xugudb_test_histo_data_bucket{label1="firstlabel",label2="secondlabel",le="+Inf"} 45
+# xugudb_test_histo_data_sum{label1="firstlabel",label2="secondlabel"} 123.45
+# xugudb_test_histo_data_count{label1="firstlabel",label2="secondlabel"} 45

+ 37 - 0
multi-metric-dual-example-labels.toml

@@ -0,0 +1,37 @@
+# 定义第一个指标配置(没有标签的例子)
+[[metric]]
+# 该指标的上下文名称,表示没有标签的情况
+context = "context_no_label"  
+# SQL 查询,返回两个固定的值:1 和 2
+request = "SELECT 1 as value_1, 2 as value_2 FROM DUAL"  
+# 对两个值的描述,分别为 1 和 2
+metricsdesc = { value_1 = "简单示例,始终返回 1。", value_2 = "同样的示例,始终返回 2。" }  
+
+#输出
+# HELP xugudb_context_no_label_value_1 简单示例,始终返回 1。
+# TYPE xugudb_context_no_label_value_1 counter
+# xugudb_context_no_label_value_1 1
+# HELP xugudb_context_no_label_value_2 同样的示例,始终返回 2。
+# TYPE xugudb_context_no_label_value_2 gauge
+# xugudb_context_no_label_value_2 2
+
+
+
+# 定义第二个指标配置(带标签的例子)
+[[metric]]
+# 该指标的上下文名称,表示带标签的情况
+context = "context_with_labels"  
+# 定义两个标签,"label_1" 和 "label_2",它们将用于区分指标的不同实例
+labels = [ "label_1", "label_2" ]  
+# SQL 查询,除了返回 1 和 2 之外,还返回两个标签值:'First label' 和 'Second label'
+request = "SELECT 1 as value_1, 2 as value_2, 'First label' as label_1, 'Second label' as label_2 FROM DUAL"  
+# 对两个值的描述,分别为 1 和 2
+metricsdesc = { value_1 = "简单示例,始终返回 1。", value_2 = "同样的示例,始终返回 2。" }  
+
+#输出
+# HELP xugudb_context_with_labels_value_1 简单示例,始终返回 1。
+# TYPE xugudb_context_with_labels_value_1 gauge
+# xugudb_context_with_labels_value_1{label_1="First label",label_2="Second label"} 1
+# HELP xugudb_context_with_labels_value_2 同样的示例,始终返回 2。
+# TYPE xugudb_context_with_labels_value_2 gauge
+# xugudb_context_with_labels_value_2{label_1="First label",label_2="Second label"} 2

+ 32 - 0
xugu_example.toml

@@ -0,0 +1,32 @@
+
+[[metric]]  # 定义一个指标配置
+# 指标的上下文或分类,这里为 "test" 表示测试用途
+context = "trans_xugu"  
+
+labels = [ "server_time", "node_id" ] 
+
+# SQL 查询,用于从 DUAL 表中选择常量值,返回两个值和两个标签
+request = "SELECT CURR_T as server_time,NODEID node_id ,ACT_TRANS_NUM  act ,LOCK_WAIT_N lwn FROM sys_run_info;"  
+  
+# 描述 value_1 为计数器类型,始终返回 1,value_2 为仪表类型,始终返回 2
+metricsdesc = { act = "当前节点活动事务数量", lwn ="锁等待数量" }  
+  
+# 指定 value_1 的类型为计数器(counter),value_2 默认为仪表(gauge)
+metricstype = { act = "gauge" , lwn = "gauge"}  
+        #计数器(counter)指的是一个只能增加或重置的值,常用于表示事件的发生次数。
+        #仪表(gauge)则是一个可以增减的值,通常用于表示某个瞬时状态或度量(如温度、内存使用量等)。
+
+
+[[metric]] # 定义一个指标配置
+context = "DISK_xugu" # 指标的上下文或分类,这里是 "test_histo",表示这是一个用于测试的直方图
+# SQL 查询,选择了多个字段来模拟直方图的不同桶(buckets)和相关的计数值
+request = "SELECT CURR_T as server_time,NODEID node_id , DISK_W_BYTES  / (1024 * 1024)  AS data ,DISK_R_BYTES / (1024 * 1024)  AS drb ,45 as count FROM sys_run_info;"
+# 描述字段 data 为直方图类型,表示数据字段的所有值的总和
+metricsdesc = { data = "Histogram - sum total of all values in the data field." }
+# 指定 data 字段的类型为直方图(histogram)
+metricstype = { data = "histogram" }
+
+labels = ["server_time", "node_id" ]
+metricsbuckets = { data = { le_20 = "20", le_40 = "40", le_60 = "60", le_80 = "80" } }
+
+