golang 编写青云 BOSS API Exporter 并对接至 K8S Prometheus

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 代码流程

思路流程如下:

  1. 链接私有云 client
  2. 通过 API 获取数据
  3. 通过 API 获取的数据写解析结构体
  4. 编写 exporter 将监控数据
  5. 编写日志功能
  6. 编写配置文件功能

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&params=%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 请求并处理响应.

代码结构

  1. Boss2Client 结构体:
    • url: BOSS API 的基础 URL。
    • access_key: 访问密钥。
    • secret_key: 秘密密钥。
    • ignore_verify: 是否忽略 TLS 证书验证。
  2. NewClient 函数:
    • 创建一个新的 Boss2Client 实例。
    • 检查协议是否为 httphttps
    • 格式化 URL 并返回 Boss2Client 实例。
  3. GenSignature 方法:
    • 生成请求签名,使用 HMAC-SHA256 算法。
    • 签名内容包括 HTTP 方法、路径、参数字符串和时间戳。
  4. Request 方法:
    • 生成请求参数字符串。
    • 生成签名并添加到请求数据中。
    • 发送 HTTP POST 请求并返回响应。
  5. Boss2DescribeLoadBalancers 方法:
    • 描述负载均衡器。
    • 生成请求参数并调用 Request 方法。
  6. DescribeLoadBalancerListeners 方法:
    • 描述负载均衡器的监听器。
    • 生成请求参数并调用 Request 方法。
  7. 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: 负载均衡器的状态,可以是 activestopped
    • LIMIT: 每页获取的数据量,这里设置为99。
    • OFFSET: 偏移量,这里设置为0,表示从第一个数据开始获取。
  • 返回值: 返回一个 models.LoadBalancerResponse 类型的指针和一个错误对象。

2.4 编写 models 代码块解析获取的 resp

这段代码定义了一个用于解析负载均衡器信息的 Go 语言包。它包括两个主要部分:

  1. 数据结构定义:定义了与 JSON 数据结构匹配的 Go 结构体,以便能够将 JSON 数据反序列化为 Go 对象。
  2. 解析函数:提供了一个函数 ParseLoadBalancers,用于将 JSON 格式的数据解析为定义好的 Go 结构体。

详细解释

数据结构定义

  1. LoadbalancerSet:
    • 这个结构体表示单个负载均衡器集的信息。
    • 包含了多个字段,如 NodeBalanceMode, BackendIPVersion, IsApplied, VxnetID, ConsoleID 等,这些字段对应于 JSON 数据中的键。
    • 嵌套的结构体和切片(如 Cluster, Listeners, SecurityGroups)进一步细化了负载均衡器集的详细信息。
  2. LoadBalancerResponse:
    • 这个结构体表示从 API 获取的负载均衡器响应的整体结构。
    • 包含一个动作 (Action)、总计数 (TotalCount)、负载均衡器集合 (LoadbalancerSets) 以及返回码 (RetCode)。

解析函数

  1. ParseLoadBalancers
    • 该函数接受一个字节数组(通常是从 HTTP 响应中读取的 JSON 数据),并将其解析为 LoadBalancerResponse 结构体。
    • 使用 json.Unmarshal 函数将 JSON 数据反序列化为 Go 结构体。如果解析过程中出现错误,会记录错误日志并打印错误信息。
    • 返回一个指向 LoadBalancerResponse 结构体的指针。

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的指标进行监控。

主要逻辑

  1. 定义Prometheus指标
    • LBStatus: 记录负载均衡器的状态。
    • LBInfo: 记录负载均衡器的详细信息。
    • LBMaxConnection: 记录负载均衡器的最大连接数。
  2. 初始化Prometheus指标
    • init函数中注册上述指标。
  3. 获取负载均衡器数据
    • FetchLoadBalancers函数从青云API获取负载均衡器数据,并合并多个分页的数据。
  4. 更新指标
    • LBMetrics函数定期(每10秒)调用FetchLoadBalancers获取最新的负载均衡器数据,并与上次的数据进行比较。如果数据没有变化,则跳过更新;否则,重置指标并更新新的数据。
  5. 判断数据是否需要更新
    • 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

  1. 设置配置文件的名称为 "config"。

           viper.SetConfigName("config")
  2. 设置配置文件的类型为 "yaml"。

           viper.SetConfigType("yaml")
  3. 添加配置文件的路径为当前目录。

           viper.AddConfigPath(".")
  4. 读取配置文件,如果读取失败,记录错误日志。

           if err := viper.ReadInConfig(); err != nil {
       logger.Error("Error reading config file, %s", err)
    }
  5. 返回配置文件中的导出器端口。

           return viper.GetString("exporter.port")

ConfigBoss

  1. 设置配置文件的名称为 "config"。

           viper.SetConfigName("config")
  2. 设置配置文件的类型为 "yaml"。

           viper.SetConfigType("yaml")
  3. 添加配置文件的路径为当前目录。

           viper.AddConfigPath(".")
  4. 读取配置文件,如果读取失败,记录错误日志并打印错误信息。

           if err := viper.ReadInConfig(); err != nil {
       logger.Error("Error reading config file, %s", err)
       fmt.Println("Error reading config file, %s", err)
    }
  5. 获取配置文件中的相关配置项,并返回这些值。

    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

  1. 打开或创建指定路径的日志文件:

    logFile, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    
  2. 如果打开文件失败,调用

    Fatal

    函数记录错误并退出程序:

    if err != nil {
       Fatal("Failed to open log file: %s", err)
       fmt.Println("Failed to open log file: %s", err)
    }
    
  3. 使用

    defer

    关键字确保在函数结束时关闭文件:

    defer logFile.Close()
    
  4. 初始化全局变量

    Log

    ,设置日志前缀和格式:

    Log = log.New(logFile, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
    

Error

  1. 使用全局变量

    Log

    记录错误信息:

    Log.Printf(format, v...)
    

Fatal

  1. 使用全局变量

    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 指标。以下是代码的详细解释:

  1. 导入包:

    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 服务器和信号处理等功能。

  2. 初始化日志记录器:

    logger.InitLog("error.log", true)

    这行代码初始化日志记录器,将错误信息记录到 error.log 文件中。

  3. 创建同步等待组和停止通道:

    var wg sync.WaitGroup
    stopChan := make(chan struct{})

    sync.WaitGroup 用于等待一组 goroutines 完成,stopChan 用于发送停止信号给 goroutines。

  4. 启动收集指标的 goroutines:

    wg.Add(1)
    go collectors.LBMetrics(&wg, stopChan)
    go collectors.ListenerMetrics(&wg, stopChan)

    这里启动了两个 goroutines,分别收集负载均衡器指标和监听器指标。每个 goroutine 在开始时调用 wg.Add(1),表示增加一个等待计数。

  5. 读取配置并设置 HTTP 服务器地址:

    port := exporter_config.LbConfig()
    addr := ":" + port

    从配置文件中读取端口号,并构建 HTTP 服务器的地址。

  6. 设置 HTTP 路由和启动服务器:

    http.Handle("/metrics", promhttp.Handler())
    http.ListenAndServe(addr, nil)

    设置 /metrics 路由,使用 Prometheus 提供的 promhttp.Handler 来处理请求,并启动 HTTP 服务器。

  7. 处理中断信号以正常关闭应用程序:

    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)

    创建一个信号通道,并通知系统在接收到 os.Interrupt(通常是 Ctrl+C)或 syscall.SIGTERM 信号时向该通道发送信号。

  8. 等待停止信号并关闭所有 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 前端验证

10.0.0.10:9120/metrics

数据获取成功

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 验证

![image-20241205105407121](./golang 编写 API exporter.assets/image-20241205105407121.png)现在只需要对接至 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 监听器累积连接数
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇