golang 编写青云 BOSS API Exporter 并对接至 K8S Prometheus
项目地址:https://github.com/As9530272755/Prometheus_exporter/tree/v1.0.beta/lb_exporter
1.1 前言:
由于公司对接的 CMD 产品没有带监控相关指标和告警,但是又想对它的负载均衡做告警处理,所以这个时候我们就需要使用到 exporter 的开发,并且将其对接之 Prometheus ,从而实现告警、绘图等相关功能,以实现对它的可观测
1.2 设计如下:
从上面图中可以看到我们的 exporter 是要主动向公司私有云平台调用对应的API获取相关监控数据,并且 Prometheus 对接 exporter 从而实现得到监控数据
1.3 代码结构
~/exporter/exporter-demoll
total 48
drwxr-xr-x 2 root root 4096 Dec 4 14:09 client/ 链接私有云包
drwxr-xr-x 2 root root 4096 Dec 4 14:18 collectors/ exporter 代码包
-rw-r--r-- 1 root root 2024 Dec 4 14:01 config.yaml 配置文件
-rw-r--r-- 1 root root 0 Dec 4 11:03 error.log 日志
drwxr-xr-x 2 root root 4096 Dec 4 14:18 exporter_config/ 配置文件代码包
-rw-r--r-- 1 root root 1894 Nov 14 14:52 go.mod
drwxr-xr-x 2 root root 4096 Dec 4 14:19 logger/ 日志代码包
-rw-r--r-- 1 root root 959 Dec 4 14:23 main.go
drwxr-xr-x 2 root root 4096 Dec 4 14:20 models/ 对应数据结构体
drwxr-xr-x 2 root root 4096 Dec 4 14:23 servser/ 对应 API 代码包
drwxr-xr-x 7 root root 4096 Nov 14 13:49 vendor/
2 代码流程
思路流程如下:
- 链接私有云 client
- 通过 API 获取数据
- 通过 API 获取的数据写解析结构体
- 编写 exporter 将监控数据
- 编写日志功能
- 编写配置文件功能
2.1 创建项目目录
mkdir exporter-demo
go mod init
go mod tidy
mkdir {client,collectors,exporter_config,logger,models,servser}
2.2 编写client代码块
package bossclient
import (
"crypto/hmac"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"exporter-demo/logger"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// 链接boss结构体
type Boss2Client struct {
url string
access_key string
secret_key string
ignore_verify bool
}
// create new client object
func NewClient(host string, port int, protocol string, access_key string, secret_key string, ignore_verify bool) (*Boss2Client, error) {
// check protocol
if protocol != "http" && protocol != "https" {
return nil, errors.New("protocol should be http or https")
}
// format url
url := fmt.Sprintf("%s://%s:%d/boss2/", protocol, host, port)
return &Boss2Client{
url: url,
access_key: access_key,
secret_key: secret_key,
ignore_verify: ignore_verify,
}, nil
}
func (b2c *Boss2Client) GenSignature(access_key string, action string, paramsStr string, timestamp string) string {
// format msg
reqStr := fmt.Sprintf("access_key_id=%s&action=%s¶ms=%s&time_stamp=%s", access_key, action, url.QueryEscape(paramsStr), url.QueryEscape(timestamp))
msg := fmt.Sprintf("POST\n/boss2/\n%s", reqStr)
// sign with secret key
digest := hmac.New(sha256.New, []byte(b2c.secret_key))
digest.Write([]byte(msg))
signature := base64.StdEncoding.EncodeToString(digest.Sum(nil))
return signature
}
func (b2c *Boss2Client) Request(action string, params map[string]interface{}) (string, error) {
// gen params string
params["action"] = action
paramsStr, _ := json.Marshal(params)
// gen signature
timestamp := time.Now().UTC().Format(time.RFC3339)
signature := b2c.GenSignature(b2c.access_key, action, string(paramsStr), timestamp)
// add signature to request body
data := url.Values{}
data.Set("action", action)
data.Set("params", string(paramsStr))
data.Set("access_key_id", b2c.access_key)
data.Set("time_stamp", timestamp)
data.Set("signature", signature)
// send post request
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: b2c.ignore_verify},
}
client := &http.Client{Transport: tr}
resp, err := client.Post(b2c.url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
if err != nil {
logger.Error("an error occured when requesting to boss", err)
fmt.Println("an error occured when requesting to boss", err)
}
defer resp.Body.Close()
//Read the response body
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("an error occured when getting response from boss, %v", err)
}
return string(responseBody), nil
}
// 负载均衡
func (b2c *Boss2Client) Boss2DescribeLoadBalancers(zone string, status []string, limit int, offset int) (string, error) {
action := "Boss2DescribeLoadBalancers"
// gen params
params := map[string]interface{}{
"zone": zone,
"status": status,
"limit": limit,
"offset": offset,
//"resources": []string{"lb-x1beyj7r"},
}
if len(status) == 0 || status == nil {
params["status"] = []string{"active"}
}
// send requests
resp, err := b2c.Request(action, params)
if err != nil {
logger.Error("Boss2DescribeLoadBalancers", err)
fmt.Println("Boss2DescribeLoadBalancers", err)
}
return resp, nil
}
// 监听器
func (b2c *Boss2Client) DescribeLoadBalancerListeners(zone string, limit int, offset int) (string, error) {
action := "DescribeLoadBalancerListeners"
// gen params
params := map[string]interface{}{
"zone": zone,
"limit": limit,
"offset": offset,
//"resources": []string{"lb-x1beyj7r"},
}
//if len(status) == 0 || status == nil {
// params["status"] = []string{"active"}
//}
// send requests
resp, err := b2c.Request(action, params)
if err != nil {
logger.Error("DescribeLoadBalancerListeners", err)
fmt.Println("DescribeLoadBalancerListeners", err)
}
return resp, nil
}
// 负载均衡
func (b2c *Boss2Client) GetLoadBalancerMonitor(zone, resource, step, protocol, subtype, startTime, endTime string, meters []string) (string, error) {
action := "GetLoadBalancerMonitor"
// gen params
params := map[string]interface{}{
"zone": zone,
"resource": resource,
"meters": meters,
"step": step,
"protocol": protocol,
"subtype": subtype,
"start_time": startTime,
"end_time": endTime,
//"resources": []string{"lb-x1beyj7r"},
}
// send requests
resp, err := b2c.Request(action, params)
if err != nil {
logger.Error("GetLoadBalancerMonitor", err)
fmt.Println("GetLoadBalancerMonitor", err)
}
return resp, nil
}
这段代码实现了一个与 BOSS API 交互的客户端,用于发送 HTTP 请求并处理响应.
代码结构
- Boss2Client 结构体:
url
: BOSS API 的基础 URL。access_key
: 访问密钥。secret_key
: 秘密密钥。ignore_verify
: 是否忽略 TLS 证书验证。
- NewClient 函数:
- 创建一个新的
Boss2Client
实例。 - 检查协议是否为
http
或https
。 - 格式化 URL 并返回
Boss2Client
实例。
- 创建一个新的
- GenSignature 方法:
- 生成请求签名,使用 HMAC-SHA256 算法。
- 签名内容包括 HTTP 方法、路径、参数字符串和时间戳。
- Request 方法:
- 生成请求参数字符串。
- 生成签名并添加到请求数据中。
- 发送 HTTP POST 请求并返回响应。
- Boss2DescribeLoadBalancers 方法:
- 描述负载均衡器。
- 生成请求参数并调用
Request
方法。
- DescribeLoadBalancerListeners 方法:
- 描述负载均衡器的监听器。
- 生成请求参数并调用
Request
方法。
- GetLoadBalancerMonitor 方法:
- 获取负载均衡器的监控信息。
- 生成请求参数并调用
Request
方法。
2.3 编写 server 代码块
这段代码定义了函数 lb_set
,每个函数都用于从青云(QingCloud)API获取负载均衡器(Load Balancers)的信息。这些函数通过调用青云的API来获取不同页码的负载均衡器数据,并将结果解析为 models.LoadBalancerResponse
类型。
以下是对每个函数的详细解释:
函数 lb_set
func lb_set() (*models.LoadBalancerResponse, error) {
host, protocol, access_key, secret_key, zone, port, ignore_verify := exporter_config.ConfigBoss()
// init client
client, err := bossclient.NewClient(host, port, protocol, access_key, secret_key, ignore_verify)
if err != nil {
logger.Error("链接至青云client error", err.Error())
fmt.Println("链接至青云client error", err.Error())
return nil, err
}
// please modify following params to send a test request
ZONE := zone
STATUS := []string{"active", "stopped"}
LIMIT := 99
OFFSET := 0
resp, err := client.Boss2DescribeLoadBalancers(ZONE, STATUS, LIMIT, OFFSET)
if err != nil {
logger.Error("青云 LoadBalancers 获取 error", err.Error())
fmt.Println("链接至青云 LoadBalancers error", err.Error())
return nil, err
}
return models.ParseLoadBalancers([]byte(resp)), nil
}
-
功能: 获取第一页前一百个负载均衡器的数据。
-
参数
:
ZONE
: 区域信息。STATUS
: 负载均衡器的状态,可以是active
或stopped
。LIMIT
: 每页获取的数据量,这里设置为99。OFFSET
: 偏移量,这里设置为0,表示从第一个数据开始获取。
-
返回值: 返回一个
models.LoadBalancerResponse
类型的指针和一个错误对象。
2.4 编写 models 代码块解析获取的 resp
这段代码定义了一个用于解析负载均衡器信息的 Go 语言包。它包括两个主要部分:
- 数据结构定义:定义了与 JSON 数据结构匹配的 Go 结构体,以便能够将 JSON 数据反序列化为 Go 对象。
- 解析函数:提供了一个函数
ParseLoadBalancers
,用于将 JSON 格式的数据解析为定义好的 Go 结构体。
详细解释
数据结构定义
- LoadbalancerSet:
- 这个结构体表示单个负载均衡器集的信息。
- 包含了多个字段,如
NodeBalanceMode
,BackendIPVersion
,IsApplied
,VxnetID
,ConsoleID
等,这些字段对应于 JSON 数据中的键。 - 嵌套的结构体和切片(如
Cluster
,Listeners
,SecurityGroups
)进一步细化了负载均衡器集的详细信息。
- LoadBalancerResponse:
- 这个结构体表示从 API 获取的负载均衡器响应的整体结构。
- 包含一个动作 (
Action
)、总计数 (TotalCount
)、负载均衡器集合 (LoadbalancerSets
) 以及返回码 (RetCode
)。
解析函数
- ParseLoadBalancers
- 该函数接受一个字节数组(通常是从 HTTP 响应中读取的 JSON 数据),并将其解析为
LoadBalancerResponse
结构体。 - 使用
json.Unmarshal
函数将 JSON 数据反序列化为 Go 结构体。如果解析过程中出现错误,会记录错误日志并打印错误信息。 - 返回一个指向
LoadBalancerResponse
结构体的指针。
- 该函数接受一个字节数组(通常是从 HTTP 响应中读取的 JSON 数据),并将其解析为
2.5 编写 exporter 代码块
package collectors
import (
"exporter-demo/models"
"exporter-demo/servser"
"github.com/prometheus/client_golang/prometheus"
"strings"
"sync"
"time"
)
var (
LBStatus = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "qingcloud_loadbalancer_status",
Help: "Status of the QingCloud LoadBalancer",
},
[]string{"zone", "lb_id", "lb_name", "lb_status", "lb_ip"},
)
LBInfo = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "qingcloud_loadbalancer_info",
Help: "Info of the QingCloud LoadBalancer",
},
[]string{"zone", "lb_id", "lb_name", "lb_status", "lb_ip", "rsyslog", "securityGroup", "privateIP"})
LBMaxConnection = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "qingcloud_loadbalancer_max_conn",
Help: "Max Connection of the QingCloud LoadBalancer",
},
[]string{"zone", "lb_id", "lb_name", "lb_status", "lb_ip"})
)
func init() {
prometheus.MustRegister(LBStatus)
prometheus.MustRegister(LBInfo)
prometheus.MustRegister(LBMaxConnection)
//prometheus.MustRegister(ListenerTotalConnections)
}
// 由于青云 LB 每次最多只能获取 100 个,所以这里将不同分页进行数据整合
func FetchLoadBalancers() []models.LoadbalancerSet {
lbs1, err := servser.lb_set()
if err != nil {
// handle error appropriately
return nil
}
// 合并获取所有 lb
allLoadBalancers := lbs1.LoadbalancerSets
return allLoadBalancers
}
func LBMetrics(wg *sync.WaitGroup, stopChan <-chan struct{}) {
defer wg.Done()
// 10s 更新一次 API,创建了一个定时器 ticker,每隔 10 秒触发一次
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
var lastLoadBalancers []models.LoadbalancerSet
for {
select {
case <-ticker.C:
loadBalancers := FetchLoadBalancers()
//listeners := FetchListeners()
//数据无变化,跳过更新指标,用久数据和新数据做对比,并且 equal 返回得数据为真才跳过更新
if len(lastLoadBalancers) == len(loadBalancers) && equal(lastLoadBalancers, loadBalancers) {
continue
}
// 重置 exporter 获取得 metrics
LBStatus.Reset()
LBInfo.Reset()
LBMaxConnection.Reset()
//ListenerTotalConnections.Reset()
// 对状态做判断得 Metrics
for _, lb := range loadBalancers {
status := 0.0
if lb.Status == "active" {
status = 1.0
} else {
status = 0.0
}
// 获取 LBIP
lbip := ""
for _, cluster := range lb.Cluster {
if cluster.EipName != "" && cluster.EipAddr != "" {
lbip = cluster.EipAddr
}
}
// 获取安全组
securityGroup := ""
for _, SecurityGroup := range lb.SecurityGroups {
if SecurityGroup.GroupName != "" {
securityGroup = SecurityGroup.GroupName
}
}
// 获取私有IP集
privateIP := strings.Join(lb.PrivateIPs, ",")
// LB状态做监控
LBStatus.WithLabelValues(lb.ZoneID, lb.LoadbalancerID, lb.LoadbalancerName, lb.Status, lbip).Set(status)
// LB信息监控
LBInfo.WithLabelValues(lb.ZoneID, lb.LoadbalancerID, lb.LoadbalancerName, lb.Status, lbip, lb.Rsyslog, securityGroup, privateIP).Set(1)
maxConn := lb.LoadbalancerType
switch maxConn {
case 0:
LBMaxConnection.WithLabelValues(lb.ZoneID, lb.LoadbalancerID, lb.LoadbalancerName, lb.Status, lbip).Set(5000)
case 1:
LBMaxConnection.WithLabelValues(lb.ZoneID, lb.LoadbalancerID, lb.LoadbalancerName, lb.Status, lbip).Set(20000)
case 2:
LBMaxConnection.WithLabelValues(lb.ZoneID, lb.LoadbalancerID, lb.LoadbalancerName, lb.Status, lbip).Set(40000)
case 3:
LBMaxConnection.WithLabelValues(lb.ZoneID, lb.LoadbalancerID, lb.LoadbalancerName, lb.Status, lbip).Set(100000)
}
lastLoadBalancers = loadBalancers
}
case <-stopChan:
return
}
}
}
// 用于判断 LB 数据是否需要更新
func equal(a, b []models.LoadbalancerSet) bool {
if len(a) != len(b) {
return false
}
for i := range a {
// 判断 LB 状态是否变化,如果有变化也需要更新数据
if a[i].Status != b[i].Status {
return false
} else if a[i].LoadbalancerType != b[i].LoadbalancerType { // 判断 LB 连接数是否变化,如果有变化也需要更新数据
return false
}
}
return true
}
代码解释:
功能:
该代码用于从青云(QingCloud)云平台获取负载均衡器(LoadBalancer)的状态和信息,并通过Prometheus的指标进行监控。
主要逻辑
- 定义Prometheus指标:
LBStatus
: 记录负载均衡器的状态。LBInfo
: 记录负载均衡器的详细信息。LBMaxConnection
: 记录负载均衡器的最大连接数。
- 初始化Prometheus指标:
- 在
init
函数中注册上述指标。
- 在
- 获取负载均衡器数据:
FetchLoadBalancers
函数从青云API获取负载均衡器数据,并合并多个分页的数据。
- 更新指标:
LBMetrics
函数定期(每10秒)调用FetchLoadBalancers
获取最新的负载均衡器数据,并与上次的数据进行比较。如果数据没有变化,则跳过更新;否则,重置指标并更新新的数据。
- 判断数据是否需要更新:
equal
函数用于比较两次获取的负载均衡器数据是否相同,以决定是否需要更新指标。
总结
该代码通过定时任务从青云API获取负载均衡器的数据,并通过Prometheus的指标进行监控。它确保了只有在数据发生变化时才更新指标,从而避免了不必要的资源消耗。
2.6 编写配置文件代码块
该功能用来实现穿的登录方式,以及 exporter 的暴露方式
代码如下:
package exporter_config
import (
"exporter-demo/logger"
"fmt"
"github.com/spf13/viper"
)
// LB config
func LbConfig() string {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
// 读取配置文件
if err := viper.ReadInConfig(); err != nil {
logger.Error("Error reading config file, %s", err)
}
return viper.GetString("exporter.port")
}
// boss config
func ConfigBoss() (host, protocol, access_key, secret_key, zone string, port int, ignore_verify bool) {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
// 读取配置文件
if err := viper.ReadInConfig(); err != nil {
logger.Error("Error reading config file, %s", err)
fmt.Println("Error reading config file, %s", err)
}
HOST := viper.GetString("boss.host")
BossPORT := viper.GetInt("boss.boss_port")
PROTOCOL := viper.GetString("boss.protocol")
ACCESS_KEY := viper.GetString("boss.access_key")
SECRET_KEY := viper.GetString("boss.secret_key")
ZONE := viper.GetString("boss.zone")
IGNORE_VERIFY := false
return HOST, PROTOCOL, ACCESS_KEY, SECRET_KEY, ZONE, BossPORT, IGNORE_VERIFY
}
目的
读取配置文件中的配置信息,并返回相应的配置值。
通过读取配置文件,获取应用程序所需的各种配置参数,如端口、主机地址、协议、访问密钥等。
使用场景
在需要从配置文件中读取配置信息的场景下使用,例如初始化应用程序时加载配置,或者在运行时动态调整配置。
函数说明
LbConfig
- 参数:无
- 返回值:返回一个字符串,表示配置文件中的导出器端口。
ConfigBoss
- 参数:无
- 返回值:返回多个值,包括主机地址、协议、访问密钥、秘密密钥、区域、端口和忽略验证标志。
主要逻辑:
LbConfig
-
设置配置文件的名称为 "config"。
viper.SetConfigName("config")
-
设置配置文件的类型为 "yaml"。
viper.SetConfigType("yaml")
-
添加配置文件的路径为当前目录。
viper.AddConfigPath(".")
-
读取配置文件,如果读取失败,记录错误日志。
if err := viper.ReadInConfig(); err != nil { logger.Error("Error reading config file, %s", err) }
-
返回配置文件中的导出器端口。
return viper.GetString("exporter.port")
ConfigBoss
-
设置配置文件的名称为 "config"。
viper.SetConfigName("config")
-
设置配置文件的类型为 "yaml"。
viper.SetConfigType("yaml")
-
添加配置文件的路径为当前目录。
viper.AddConfigPath(".")
-
读取配置文件,如果读取失败,记录错误日志并打印错误信息。
if err := viper.ReadInConfig(); err != nil { logger.Error("Error reading config file, %s", err) fmt.Println("Error reading config file, %s", err) }
-
获取配置文件中的相关配置项,并返回这些值。
HOST := viper.GetString("boss.host") BossPORT := viper.GetInt("boss.boss_port") PROTOCOL := viper.GetString("boss.protocol") ACCESS_KEY := viper.GetString("boss.access_key") SECRET_KEY := viper.GetString("boss.secret_key") ZONE := viper.GetString("boss.zone") IGNORE_VERIFY := false return HOST, PROTOCOL, ACCESS_KEY, SECRET_KEY, ZONE, BossPORT, IGNORE_VERIFY
总结
这段代码定义了两个函数,用于从配置文件中读取不同的配置信息。然而,这段代码似乎有错误,具体如下:
2.7 编写日志功能
// logger/logger.go
package logger
import (
"fmt"
"log"
"os"
)
var Log *log.Logger
func InitLog(filePath string, exitOnFatal bool) {
logFile, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
Fatal("Failed to open log file: %s", err)
fmt.Println("Failed to open log file: %s", err)
}
defer logFile.Close()
Log = log.New(logFile, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
}
func Error(format string, v ...interface{}) {
Log.Printf(format, v...)
}
func Fatal(format string, v ...interface{}) {
Log.Fatalf(format, v...)
}
功能
初始化日志记录器,并定义了两个函数用于记录错误信息和致命错误信息。
目的
提供一个统一的日志记录机制,方便在程序中记录错误和致命错误信息,并将这些信息写入指定的日志文件中。
使用场景
适用于需要记录运行时错误和致命错误的应用程序,特别是在调试和维护过程中,通过日志文件可以快速定位问题。
函数说明
InitLog
-
参数
:
filePath
(string): 日志文件的路径。exitOnFatal
(bool): 是否在记录致命错误后退出程序。
-
返回值: 无
Error
-
参数
:
format
(string): 格式化字符串。v
(…interface{}): 可变参数列表,用于格式化字符串中的占位符。
-
返回值: 无
Fatal
-
参数
:
format
(string): 格式化字符串。v
(…interface{}): 可变参数列表,用于格式化字符串中的占位符。
-
返回值: 无
主要逻辑
InitLog
-
打开或创建指定路径的日志文件:
logFile, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
-
如果打开文件失败,调用
Fatal
函数记录错误并退出程序:
if err != nil { Fatal("Failed to open log file: %s", err) fmt.Println("Failed to open log file: %s", err) }
-
使用
defer
关键字确保在函数结束时关闭文件:
defer logFile.Close()
-
初始化全局变量
Log
,设置日志前缀和格式:
Log = log.New(logFile, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
Error
-
使用全局变量
Log
记录错误信息:
Log.Printf(format, v...)
Fatal
-
使用全局变量
Log
记录致命错误信息,并终止程序:
Log.Fatalf(format, v...)
总结
这段代码实现了一个基本的日志记录系统,包括初始化日志文件、记录错误信息和致命错误信息的功能。
2.8 main 函数代码整合
package main
import (
"exporter-demo/collectors"
"exporter-demo/exporter_config"
"exporter-demo/logger"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
)
func main() {
//collectors.FetchListeners()
logger.InitLog("error.log", true)
var wg sync.WaitGroup
stopChan := make(chan struct{})
wg.Add(1)
go collectors.LBMetrics(&wg, stopChan)
go collectors.ListenerMetrics(&wg, stopChan)
port := exporter_config.LbConfig()
addr := ":" + port
//internal.WJLoadbalancerMetrics()
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(addr, nil)
//等待中断信号以正常关闭应用程序,接收信号(通常是 Ctrl+C)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
//向goroutine发出停止信号
//close(stopChan)
<-stopChan
//等待所有goroutines完成
wg.Wait()
}
这个 main
函数是一个典型的 Go 应用程序入口点,用于启动一个 HTTP 服务器并处理 Prometheus 指标。以下是代码的详细解释:
-
导入包:
import ( "exporter-demo/collectors" "exporter-demo/exporter_config" "exporter-demo/logger" "github.com/prometheus/client_golang/prometheus/promhttp" "net/http" "os" "os/signal" "sync" "syscall" )
这些包提供了日志记录、配置读取、HTTP 服务器和信号处理等功能。
-
初始化日志记录器:
logger.InitLog("error.log", true)
这行代码初始化日志记录器,将错误信息记录到
error.log
文件中。 -
创建同步等待组和停止通道:
var wg sync.WaitGroup stopChan := make(chan struct{})
sync.WaitGroup
用于等待一组 goroutines 完成,stopChan
用于发送停止信号给 goroutines。 -
启动收集指标的 goroutines:
wg.Add(1) go collectors.LBMetrics(&wg, stopChan) go collectors.ListenerMetrics(&wg, stopChan)
这里启动了两个 goroutines,分别收集负载均衡器指标和监听器指标。每个 goroutine 在开始时调用
wg.Add(1)
,表示增加一个等待计数。 -
读取配置并设置 HTTP 服务器地址:
port := exporter_config.LbConfig() addr := ":" + port
从配置文件中读取端口号,并构建 HTTP 服务器的地址。
-
设置 HTTP 路由和启动服务器:
http.Handle("/metrics", promhttp.Handler()) http.ListenAndServe(addr, nil)
设置
/metrics
路由,使用 Prometheus 提供的promhttp.Handler
来处理请求,并启动 HTTP 服务器。 -
处理中断信号以正常关闭应用程序:
c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM)
创建一个信号通道,并通知系统在接收到
os.Interrupt
(通常是 Ctrl+C)或syscall.SIGTERM
信号时向该通道发送信号。 -
等待停止信号并关闭所有 goroutines:
<-stopChan wg.Wait()
阻塞主线程,直到从
stopChan
接收到停止信号。然后等待所有 goroutines 完成。
总结来说,这段代码的主要功能是启动一个 HTTP 服务器,提供 Prometheus 指标,并在接收到中断信号时优雅地关闭所有正在运行的 goroutines。
3 编写配置文件
# vim config.yaml
# exporter 前端端口
exporter:
port: 9120
# 链接 boss 登录方式
boss:
zone: "xxx"
host: "boss.xxx.xxx.com.cn"
boss_port: 80
protocol: "http"
access_key: "xxxx"
secret_key: "xxx"
4 编写 Dockerfile 制作镜像
# 将 dockerfile 文件和 exporter-demo 项目放到一个目录中
/exporter# ls
Dockerfile exporter-demo
编写 dockerfile
# cat Dockerfile
# 使用官方的 Golang 基础镜像进行构建
FROM golang:alpine AS builder
# 设置工作目录
WORKDIR /app
# 复制源代码到容器中
COPY exporter-demo .
# 编译 Golang 程序
RUN go build -o lb_exporter .
# 使用一个更小的基础镜像来运行应用
FROM alpine:latest
# 设置工作目录
WORKDIR /root/
# 从构建阶段复制编译好的二进制文件
COPY --from=builder /app/lb_exporter .
# 从构建阶段复制配置文件到容器中
COPY --from=builder /app/config.yaml .
# 暴露端口(根据你的应用需求)
EXPOSE 9120
# 启动应用
CMD ["./lb_exporter"]
编译 dockerfile
docker build -t lb_exporter:v1 .
# 运行 lb_exporter:v1 镜像
docker run -d --name ex -p 9120:9120 lb_exporter:v1
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
de85b7880263 lb_exporter:v1 "./lb_exporter" About an hour ago Up About an hour 0.0.0.0:9120->9120/tcp, :::9120->9120/tcp ex
4.1 前端验证
数据获取成功
5 部署至 K8S 集群中
以下是具体步骤:
deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: lb-exporter
spec:
replicas: 1
selector:
matchLabels:
app: lb-exporter
template:
metadata:
labels:
app: lb-exporter
spec:
# 实现给 pod 添加主机头
hostAliases:
- ip: "xxx.xxx.xxx"
hostnames:
- "boss.xxx.xxx.com.cn"
containers:
- name: lb-exporter
image: lb_exporter:v1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9120
resources:
limits:
cpu: 2000m
memory: 2Gi
requests:
cpu: 100m
memory: 100Mi
volumeMounts:
- name: config-volume
mountPath: /root/config.yaml
subPath: config.yaml
volumes:
- name: config-volume
configMap:
name: lb-exporter-config
创建 ConfigMap: 创建一个 ConfigMap 来存储你的配置文件。
configmap.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: lb-exporter-config
data:
config.yaml: |
# exporter 前端端口
exporter:
port: 9120
# 链接 boss 登录方式
boss:
zone: "xxx"
host: "boss.xxx.xxx.com.cn"
boss_port: 80
protocol: "http"
access_key: "xxxx"
secret_key: "xxx"
创建 service 文件
apiVersion: v1
kind: Service
metadata:
labels:
# 用于给 endpoint 实现标签绑定
app: lb-exporter
name: lb-exporter
namespace: test-vm
spec:
selector:
app: lb-exporter
ports:
- name: metrics
protocol: TCP
port: 9120
targetPort: 9120
应用这些配置
kubectl apply -f configmap.yaml
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
通过这种方式,你可以在不修改 Dockerfile 的情况下,通过 hostAliases
在应用容器启动之前添加自定义主机头。这样,你的应用就可以通过 /etc/hosts
的方式访问 boss。
5.1 验证
现在只需要对接至 Prometheus 即可
6 对接 Prometheus
由于这里 Prometheus 是部署在 K8S 中所以需要 sm 资源将其暴露对应 endpoint
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
app.kubernetes.io/vendor: kubesphere
app: lb-exporter
name: lb-exporter
namespace: kubesphere-monitoring-system
spec:
namespaceSelector:
matchNames:
- test-vm
# selector 字段中匹配 endpoint 的标签
selector:
matchLabels:
app: lb-exporter
endpoints:
- interval: 1m
port: metrics # 匹配 svc port 名称
path: /metrics # 匹配监控指标路径
jobLabel: app
6.1 Prometheus 前端验证
7 指标说明
# 监控指标字段解释
qingcloud_loadbalancer_status
qingcloud_loadbalancer_status{container="lb-exporter", endpoint="metrics", instance="10.234.25.190:9120", job="lb-exporter-bjdevzj", lb_id="lb-0reyibzs", lb_name="csec-agent-cdev", lb_status="active",namespace="test-vm", pod="lb-exporter-bjdevzj-6f56d89b6d-rdqhs", service="lb-exporter-bjdevzj", zone="bjdevzj1"}
# qingcloud_loadbalancer_status 负载均衡器状态为 1 表示正常,0 表示不正常
# job job 为后端 lb-exporter-bjdevzj pod
# lb_id LB ID
# lb_name LB 名称
# lb_status LB 状态为活跃
# zone LB 所属 zone
qingcloud_loadbalancer_info
qingcloud_loadbalancer_info{container="lb-exporter", endpoint="metrics", instance="10.234.25.190:9120", job="lb-exporter-bjdevzj", lb_id="lb-0reyibzs", lb_name="csec-agent-cdev", lb_status="active", namespace="test-vm", pod="lb-exporter-bjdevzj-6f56d89b6d-rdqhs", privateIP="10.111.142.218,198.19.1.37,10.111.142.63,198.19.1.36,10.111.142.62", rsyslog="10.111.143.70", securityGroup="全通组", service="lb-exporter-bjdevzj", zone="bjdevzj1"}
# qingcloud_loadbalancer_info 负载均衡相关信息
# privateIP 私有IP
# rsyslog rsyslog IP
# securityGroup 分组信息
qingcloud_loadbalancer_max_conn
qingcloud_loadbalancer_max_conn{container="lb-exporter", endpoint="metrics", instance="10.234.25.190:9120", job="lb-exporter-bjdevzj", lb_id="lb-0reyibzs", lb_name="csec-agent-cdev", lb_status="active", namespace="test-vm", pod="lb-exporter-bjdevzj-6f56d89b6d-rdqhs", service="lb-exporter-bjdevzj", zone="bjdevzj1"}
# qingcloud_loadbalancer_max_conn 负载均衡最大连接数
qingcloud_listener_max_concurrency
qingcloud_listener_max_concurrency{container="lb-exporter", endpoint="metrics", instance="10.234.25.190:9120", istener_id="lbl-387acvw0", job="lb-exporter-bjdevzj", lb_id="lb-0reyibzs", listener_name="listener_csec-agent-cdev_dosec-server_", namespace="test-vm", pod="lb-exporter-bjdevzj-6f56d89b6d-rdqhs", service="lb-exporter-bjdevzj", zone="bjdevzj1"}
# qingcloud_listener_max_concurrency 监听器最大并发数
# istener_id 监听器ID
# lb_id 负载均衡 ID
# listener_name 监听器名称
# zone 监听器所属 zone
qingcloud_listener_concurrent_limit
qingcloud_listener_concurrent_limit{container="lb-exporter", endpoint="metrics", instance="10.234.25.190:9120", istener_id="lbl-387acvw0", job="lb-exporter-bjdevzj", lb_id="lb-0reyibzs", listener_name="sys-log", namespace="test-vm", pod="lb-exporter-bjdevzj-6f56d89b6d-rdqhs", service="lb-exporter-bjdevzj", zone="bjdevzj1"}
# qingcloud_listener_concurrent_limit 监听器并发连接数上限
qingcloud_listener_average_concurrency
# qingcloud_listener_average_concurrency 监听器并发连接数平均值
qingcloud_listener_accumulated_connections
# qingcloud_listener_accumulated_connections 监听器累积连接数