2023-04-25 20:05:47 +02:00
|
|
|
package collector
|
|
|
|
|
|
|
|
import (
|
2024-05-21 14:00:31 +02:00
|
|
|
"context"
|
2023-04-25 20:05:47 +02:00
|
|
|
"fmt"
|
2023-05-03 18:13:02 +02:00
|
|
|
"regexp"
|
2023-05-03 20:49:14 +02:00
|
|
|
"strconv"
|
2023-05-03 18:13:02 +02:00
|
|
|
"strings"
|
2023-04-25 20:05:47 +02:00
|
|
|
"time"
|
|
|
|
|
2023-05-05 12:05:09 +02:00
|
|
|
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
2023-04-25 20:05:47 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2023-05-23 15:40:56 +02:00
|
|
|
ExternalRPSMetricType = "requests-per-second"
|
2023-06-29 15:03:11 +02:00
|
|
|
ExternalRPSQuery = `scalar(sum(rate(%s{host=~"%s"}[1m])) * %.4f)`
|
2023-04-25 20:05:47 +02:00
|
|
|
)
|
|
|
|
|
2023-05-23 15:40:56 +02:00
|
|
|
type ExternalRPSCollectorPlugin struct {
|
2023-04-25 20:05:47 +02:00
|
|
|
metricName string
|
|
|
|
promPlugin CollectorPlugin
|
2023-05-08 16:16:23 +02:00
|
|
|
pattern *regexp.Regexp
|
2023-04-25 20:05:47 +02:00
|
|
|
}
|
|
|
|
|
2023-05-23 15:40:56 +02:00
|
|
|
type ExternalRPSCollector struct {
|
2023-04-25 20:05:47 +02:00
|
|
|
interval time.Duration
|
|
|
|
promCollector Collector
|
|
|
|
}
|
|
|
|
|
2023-05-23 15:40:56 +02:00
|
|
|
func NewExternalRPSCollectorPlugin(
|
2023-04-25 20:05:47 +02:00
|
|
|
promPlugin CollectorPlugin,
|
|
|
|
metricName string,
|
2023-05-23 15:40:56 +02:00
|
|
|
) (*ExternalRPSCollectorPlugin, error) {
|
2023-04-25 20:05:47 +02:00
|
|
|
if metricName == "" {
|
2023-05-15 22:39:33 +02:00
|
|
|
return nil, fmt.Errorf("failed to initialize hostname collector plugin, metric name was not defined")
|
2023-04-25 20:05:47 +02:00
|
|
|
}
|
|
|
|
|
2023-05-08 16:16:23 +02:00
|
|
|
p, err := regexp.Compile("^[a-zA-Z0-9.-]+$")
|
|
|
|
if err != nil {
|
2023-05-15 22:39:33 +02:00
|
|
|
return nil, fmt.Errorf("failed to create regular expression to match hostname format")
|
2023-05-08 16:16:23 +02:00
|
|
|
}
|
|
|
|
|
2023-05-23 15:40:56 +02:00
|
|
|
return &ExternalRPSCollectorPlugin{
|
2023-04-25 20:05:47 +02:00
|
|
|
metricName: metricName,
|
|
|
|
promPlugin: promPlugin,
|
2023-05-08 16:16:23 +02:00
|
|
|
pattern: p,
|
2023-04-25 20:05:47 +02:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewCollector initializes a new skipper collector from the specified HPA.
|
2023-05-23 15:40:56 +02:00
|
|
|
func (p *ExternalRPSCollectorPlugin) NewCollector(
|
2024-05-21 14:00:31 +02:00
|
|
|
ctx context.Context,
|
2023-04-25 20:05:47 +02:00
|
|
|
hpa *autoscalingv2.HorizontalPodAutoscaler,
|
|
|
|
config *MetricConfig,
|
|
|
|
interval time.Duration,
|
|
|
|
) (Collector, error) {
|
|
|
|
if config == nil {
|
2023-05-15 22:39:33 +02:00
|
|
|
return nil, fmt.Errorf("metric config not present, it is not possible to initialize the collector")
|
2023-04-25 20:05:47 +02:00
|
|
|
}
|
|
|
|
// Need to copy config and add a promQL query in order to get
|
|
|
|
// RPS data from a specific hostname from prometheus. The idea
|
|
|
|
// of the copy is to not modify the original config struct.
|
|
|
|
confCopy := *config
|
|
|
|
|
2023-05-03 20:49:14 +02:00
|
|
|
if _, ok := config.Config["hostnames"]; !ok {
|
2023-05-04 16:59:00 +02:00
|
|
|
return nil, fmt.Errorf("Hostname is not specified, unable to create collector")
|
|
|
|
}
|
2023-05-08 16:16:23 +02:00
|
|
|
|
2023-05-03 20:49:14 +02:00
|
|
|
hostnames := strings.Split(config.Config["hostnames"], ",")
|
2023-05-08 16:16:23 +02:00
|
|
|
if p.pattern == nil {
|
2023-05-15 22:39:33 +02:00
|
|
|
return nil, fmt.Errorf("plugin did not specify hostname regex pattern, unable to create collector")
|
2023-05-08 16:16:23 +02:00
|
|
|
}
|
2023-05-03 20:49:14 +02:00
|
|
|
for _, h := range hostnames {
|
2023-05-08 16:16:23 +02:00
|
|
|
if ok := p.pattern.MatchString(h); !ok {
|
2023-05-03 20:49:14 +02:00
|
|
|
return nil, fmt.Errorf(
|
2023-05-15 22:39:33 +02:00
|
|
|
"invalid hostname format, unable to create collector: %s",
|
2023-05-03 20:49:14 +02:00
|
|
|
h,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
weight := 1.0
|
|
|
|
if w, ok := config.Config["weight"]; ok {
|
|
|
|
num, err := strconv.ParseFloat(w, 64)
|
|
|
|
if err != nil {
|
2023-05-15 22:39:33 +02:00
|
|
|
return nil, fmt.Errorf("could not parse weight annotation, unable to create collector: %s", w)
|
2023-05-03 20:49:14 +02:00
|
|
|
}
|
|
|
|
weight = num / 100.0
|
2023-04-25 20:05:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
confCopy.Config = map[string]string{
|
2023-05-03 18:13:02 +02:00
|
|
|
"query": fmt.Sprintf(
|
2023-05-23 15:40:56 +02:00
|
|
|
ExternalRPSQuery,
|
2023-05-03 18:13:02 +02:00
|
|
|
p.metricName,
|
2023-06-29 15:03:11 +02:00
|
|
|
strings.ReplaceAll(strings.Join(hostnames, "|"), ".", "_"),
|
2023-05-03 20:49:14 +02:00
|
|
|
weight,
|
2023-05-03 18:13:02 +02:00
|
|
|
),
|
2023-04-25 20:05:47 +02:00
|
|
|
}
|
|
|
|
|
2024-05-21 14:00:31 +02:00
|
|
|
c, err := p.promPlugin.NewCollector(ctx, hpa, &confCopy, interval)
|
2023-04-25 20:05:47 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-05-23 15:40:56 +02:00
|
|
|
return &ExternalRPSCollector{
|
2023-04-25 20:05:47 +02:00
|
|
|
interval: interval,
|
|
|
|
promCollector: c,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetMetrics gets hostname metrics from Prometheus
|
2024-05-21 14:00:31 +02:00
|
|
|
func (c *ExternalRPSCollector) GetMetrics(ctx context.Context) ([]CollectedMetric, error) {
|
|
|
|
v, err := c.promCollector.GetMetrics(ctx)
|
2023-04-25 20:05:47 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(v) != 1 {
|
2023-05-15 22:39:33 +02:00
|
|
|
return nil, fmt.Errorf("expected to only get one metric value, got %d", len(v))
|
2023-04-25 20:05:47 +02:00
|
|
|
}
|
|
|
|
return v, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Interval returns the interval at which the collector should run.
|
2023-05-23 15:40:56 +02:00
|
|
|
func (c *ExternalRPSCollector) Interval() time.Duration {
|
2023-04-25 20:05:47 +02:00
|
|
|
return c.interval
|
|
|
|
}
|