action/prometheus/metrics.go (120 lines of code) (raw):
package prometheus
import (
"encoding/json"
"fmt"
"github.com/guptarohit/asciigraph"
"github.com/seata/seata-ctl/action/k8s/utils"
"github.com/seata/seata-ctl/model"
"github.com/seata/seata-ctl/tool"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strconv"
)
var MetricsCmd = &cobra.Command{
Use: "metrics",
Short: "Show Prometheus metrics",
Run: func(cmd *cobra.Command, args []string) {
if err := showMetrics(); err != nil {
tool.Logger.Errorf("Failed to show metrics: %v", err)
}
},
}
func init() {
MetricsCmd.PersistentFlags().StringVar(&Target, "target", DefaultPromTarget, "seata prometheus metrics name")
}
// showMetrics executes the metrics collection and chart generation
func showMetrics() error {
prometheusURL, err := getPrometheusAddress()
if err != nil {
return err
}
// Query Prometheus for metrics
result, err := queryPromMetric(prometheusURL, Target)
if err != nil {
return fmt.Errorf("query prometheus metrics: %v", err)
}
// Generate terminal chart from the queried results
if err = generateTerminalLineChart(result, Target); err != nil {
return err
}
return nil
}
// getPrometheusAddress fetches Prometheus server address from configuration
func getPrometheusAddress() (string, error) {
file, err := os.ReadFile(utils.ConfigFileName)
if err != nil {
return "", fmt.Errorf("failed to read config.yml: %v", err)
}
// Parse the configuration
var config model.Config
if err = yaml.Unmarshal(file, &config); err != nil {
return "", fmt.Errorf("failed to unmarshal YAML: %v", err)
}
// Extract Prometheus address based on context
contextName := config.Context.Prometheus
var contextPath string
for _, server := range config.Prometheus.Servers {
if server.Name == contextName {
contextPath = server.Address
}
}
if contextPath == "" {
return "", fmt.Errorf("failed to find Prometheus context in config.yml")
}
return contextPath, nil
}
// PromResponse defines the structure of a Prometheus query response
type PromResponse struct {
Status string `json:"status"`
Data struct {
ResultType string `json:"resultType"`
Result []struct {
Metric map[string]string `json:"metric"`
Value []interface{} `json:"value"`
} `json:"result"`
} `json:"data"`
}
// queryPromsMetric sends a query to the Prometheus API and returns the response
func queryPromMetric(prometheusURL, query string) (*PromResponse, error) {
queryURL := fmt.Sprintf("%s/api/v1/query?query=%s", prometheusURL, url.QueryEscape(query))
resp, err := http.Get(queryURL)
if err != nil {
return nil, fmt.Errorf("error querying Prometheus: %w", err)
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
tool.Logger.Errorf("failed to close response body: %v", err)
return
}
}(resp.Body)
// Read the response body
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading response body: %w", err)
}
// Parse JSON response into the PrometheusResponse structure
var result PromResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("error unmarshalling JSON: %w", err)
}
return &result, nil
}
// generateTerminalLineChart generates and prints an ASCII line chart based on the Prometheus response
func generateTerminalLineChart(response *PromResponse, metricName string) error {
var yValues []float64
// Iterate over the results and extract the values for the specified metric
for _, result := range response.Data.Result {
if name, ok := result.Metric["__name__"]; ok && name == metricName {
// Parse the metric value
if valueStr, ok := result.Value[1].(string); ok {
value, err := strconv.ParseFloat(valueStr, 64)
if err != nil {
return fmt.Errorf("error converting value to float: %v", err)
} else {
yValues = append(yValues, value)
}
} else {
return fmt.Errorf("error converting value to float: %v", result.Value[1])
}
}
}
// Check if any values were found
if len(yValues) == 0 {
return fmt.Errorf("no data found for metric: %s", metricName)
}
// Generate and display the ASCII line chart
graph := asciigraph.Plot(yValues, asciigraph.Width(50), asciigraph.Height(10), asciigraph.Caption(metricName))
tool.Logger.Infof(graph)
return nil
}