* Copyright (c) 2025 Bocloud Technologies Co., Ltd.
* installer is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain n copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
******************************************************************/
package metrics
import (
"fmt"
"net/http"
"time"
"k8s.io/apimachinery/pkg/util/json"
"gopkg.openfuyao.cn/cluster-api-provider-bke/utils/log"
)
const (
UTCOffsetHours = 8
)
type HttpResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"`
}
func (res HttpResponse) bytes() ([]byte, error) {
return json.Marshal(res)
}
func newResp(data interface{}, msg string, code int) HttpResponse {
return HttpResponse{
Code: code,
Msg: msg,
Data: data,
}
}
func Ok(data interface{}, msg string) ([]byte, error) {
return newResp(data, msg, http.StatusOK).bytes()
}
func Error(msg string) ([]byte, error) {
return newResp(nil, msg, http.StatusInternalServerError).bytes()
}
func Bad(msg string) ([]byte, error) {
return newResp(nil, msg, http.StatusBadRequest).bytes()
}
type httpExportResponse struct {
Name string `json:"name"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
Describe string `json:"describe"`
Duration int `json:"duration"`
}
type ParamsResult struct {
Cluster string
From string
To string
Err error
}
func (r *BKEClusterMetricRegister) extractAndValidateParams(req *http.Request) ParamsResult {
cluster := req.URL.Query().Get("cluster")
if cluster == "" {
return ParamsResult{
Cluster: "",
From: "",
To: "",
Err: fmt.Errorf("param cluster is required"),
}
}
from := req.URL.Query().Get("from")
to := req.URL.Query().Get("to")
return ParamsResult{
Cluster: cluster,
From: from,
To: to,
Err: nil,
}
}
func (r *BKEClusterMetricRegister) validateTimeRange(from, to string) (time.Time, time.Time, error) {
startTime, endTime, err := ParseTimeFromTo(from, to)
if err != nil {
return time.Time{}, time.Time{}, err
}
return startTime, endTime, nil
}
func (r *BKEClusterMetricRegister) collectAndFilterMetrics(cluster string,
startTime, endTime time.Time) (map[string][]map[string]string, error) {
metricsGather, err := r.Gather(cluster, PhaseDurationSeconds, NodeBootstrapDurationSeconds)
if err != nil {
return nil, err
}
return filterMetricsData(metricsGather, startTime, endTime), nil
}
func (r *BKEClusterMetricRegister) formatMetricsResponse(metricsGather map[string][]map[string]string) []httpExportResponse {
return ProcessMetrics(metricsGather)
}
func (r *BKEClusterMetricRegister) HttpExportFunc() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
paramsResult := r.extractAndValidateParams(req)
if paramsResult.Err != nil {
r.writeErrorResponse(w, Bad, paramsResult.Err.Error())
return
}
cluster := paramsResult.Cluster
from := paramsResult.From
to := paramsResult.To
startTime, endTime, err := r.validateTimeRange(from, to)
if err != nil {
log.Errorf("parse time error: %v", err)
r.writeErrorResponse(w, Bad, fmt.Sprintf("parse time error: %v", err))
return
}
log.Debugf("gather cluster: %q from %q to %q", cluster, startTime, endTime)
metricsGather, err := r.collectAndFilterMetrics(cluster, startTime, endTime)
if err != nil {
r.writeErrorResponse(w, Error, fmt.Sprintf("gather cluster: %s, error: %v", cluster, err))
return
}
resp := r.formatMetricsResponse(metricsGather)
res, err := Ok(resp, "export success")
if err != nil {
log.Errorf("failed to create Ok response: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if _, err = w.Write(res); err != nil {
log.Errorf("write resp error: %v", err)
return
}
}
}
func (r *BKEClusterMetricRegister) writeErrorResponse(w http.ResponseWriter,
responseFunc func(string) ([]byte, error), message string) {
res, err := responseFunc(message)
if err != nil {
log.Errorf("failed to create error response: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if _, err := w.Write(res); err != nil {
log.Errorf("write error response error: %v", err)
return
}
}
func (r *BKEClusterMetricRegister) HttpClusterFunc() http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
var resp []string
r.mux.RLock()
for clusterName := range r.collectors {
resp = append(resp, clusterName)
}
r.mux.RUnlock()
res, err := Ok(resp, "success")
if err != nil {
log.Errorf("failed to create Ok response: %v", err)
http.Error(writer, "Internal Server Error", http.StatusInternalServerError)
return
}
if _, err := writer.Write(res); err != nil {
log.Errorf("write response error: %v", err)
return
}
}
}
func calculateTimeDifference(startTimeStr, endTimeStr string) int {
startTime, err := time.Parse(TimeFormat, startTimeStr)
if err != nil {
log.Warnf("Error parsing start time: %v", err)
return 0
}
endTime, err := time.Parse(TimeFormat, endTimeStr)
if err != nil {
log.Warnf("Error parsing end time: %v", err)
return 0
}
duration := endTime.Sub(startTime)
seconds := int(duration.Seconds())
if seconds < 1 {
return 0
}
return seconds
}
func filterMetricsData(metricsGather map[string][]map[string]string,
start, end time.Time) map[string][]map[string]string {
res := make(map[string][]map[string]string)
for name, gatherMetrics := range metricsGather {
resItem := make([]map[string]string, 0)
for _, metric := range gatherMetrics {
startTime := metric["start_time"]
endTime := metric["end_time"]
if startTime == "" || endTime == "" {
continue
}
startTimeTime, err := time.Parse(TimeFormat, startTime)
if err != nil {
log.Warnf("Error parsing start time %s: %v", startTime, err)
continue
}
endTimeTime, err := time.Parse(TimeFormat, endTime)
if err != nil {
log.Warnf("Error parsing end time %s: %v", endTime, err)
continue
}
if startTimeTime.Local().Add(-UTCOffsetHours*time.Hour).After(start) &&
endTimeTime.Local().Add(-UTCOffsetHours*time.Hour).Before(end) {
resItem = append(resItem, metric)
}
}
res[name] = resItem
log.Debugf("gather vec: %q %d match time range", name, len(resItem))
}
return res
}