mirror of
https://github.com/zalando-incubator/kube-metrics-adapter.git
synced 2025-08-15 11:09:33 +00:00
Simple ZMON collector implementation (#2)
* Simple ZMON collector implementation Signed-off-by: Mikkel Oscar Lyderik Larsen <mikkel.larsen@zalando.de> * Add tests for ZMON client Signed-off-by: Mikkel Oscar Lyderik Larsen <mikkel.larsen@zalando.de> * Add tests for zmon collector Signed-off-by: Mikkel Oscar Lyderik Larsen <mikkel.larsen@zalando.de> * Update ZMON collector docs Signed-off-by: Mikkel Oscar Lyderik Larsen <mikkel.larsen@zalando.de> * Expose tags instead of entities for queries Signed-off-by: Mikkel Oscar Lyderik Larsen <mikkel.larsen@zalando.de> * Remove unused function Signed-off-by: Mikkel Oscar Lyderik Larsen <mikkel.larsen@zalando.de>
This commit is contained in:

committed by
Arjun

parent
b18acf3ed0
commit
c86a82ca88
175
pkg/collector/zmon_collector.go
Normal file
175
pkg/collector/zmon_collector.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zalando-incubator/kube-metrics-adapter/pkg/zmon"
|
||||
autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/metrics/pkg/apis/external_metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
// ZMONCheckMetric defines the metric name for metrics based on ZMON
|
||||
// checks.
|
||||
ZMONCheckMetric = "zmon-check"
|
||||
zmonCheckIDLabelKey = "check-id"
|
||||
zmonKeyLabelKey = "key"
|
||||
zmonDurationLabelKey = "duration"
|
||||
zmonAggregatorsLabelKey = "aggregators"
|
||||
zmonTagPrefixLabelKey = "tag-"
|
||||
defaultQueryDuration = 10 * time.Minute
|
||||
zmonKeyAnnotationKey = "metric-config.external.zmon-check.zmon/key"
|
||||
zmonTagPrefixAnnotationKey = "metric-config.external.zmon-check.zmon/tag-"
|
||||
)
|
||||
|
||||
// ZMONCollectorPlugin defines a plugin for creating collectors that can get
|
||||
// metrics from ZMON.
|
||||
type ZMONCollectorPlugin struct {
|
||||
zmon zmon.ZMON
|
||||
}
|
||||
|
||||
// NewZMONCollectorPlugin initializes a new ZMONCollectorPlugin.
|
||||
func NewZMONCollectorPlugin(zmon zmon.ZMON) (*ZMONCollectorPlugin, error) {
|
||||
return &ZMONCollectorPlugin{
|
||||
zmon: zmon,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewCollector initializes a new ZMON collector from the specified HPA.
|
||||
func (c *ZMONCollectorPlugin) NewCollector(hpa *autoscalingv2beta1.HorizontalPodAutoscaler, config *MetricConfig, interval time.Duration) (Collector, error) {
|
||||
switch config.Name {
|
||||
case ZMONCheckMetric:
|
||||
annotations := map[string]string{}
|
||||
if hpa != nil {
|
||||
annotations = hpa.Annotations
|
||||
}
|
||||
return NewZMONCollector(c.zmon, config, annotations, interval)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("metric '%s' not supported", config.Name)
|
||||
}
|
||||
|
||||
// ZMONCollector defines a collector that is able to collect metrics from ZMON.
|
||||
type ZMONCollector struct {
|
||||
zmon zmon.ZMON
|
||||
interval time.Duration
|
||||
checkID int
|
||||
key string
|
||||
labels map[string]string
|
||||
tags map[string]string
|
||||
duration time.Duration
|
||||
aggregators []string
|
||||
metricName string
|
||||
metricType autoscalingv2beta1.MetricSourceType
|
||||
}
|
||||
|
||||
// NewZMONCollector initializes a new ZMONCollector.
|
||||
func NewZMONCollector(zmon zmon.ZMON, config *MetricConfig, annotations map[string]string, interval time.Duration) (*ZMONCollector, error) {
|
||||
checkIDStr, ok := config.Labels[zmonCheckIDLabelKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ZMON check ID not specified on metric")
|
||||
}
|
||||
|
||||
checkID, err := strconv.Atoi(checkIDStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := ""
|
||||
|
||||
// get optional key
|
||||
if k, ok := config.Labels[zmonKeyLabelKey]; ok {
|
||||
key = k
|
||||
}
|
||||
|
||||
// annotations takes precedence over label
|
||||
if k, ok := annotations[zmonKeyAnnotationKey]; ok {
|
||||
key = k
|
||||
}
|
||||
|
||||
duration := defaultQueryDuration
|
||||
|
||||
// parse optional duration value
|
||||
if d, ok := config.Labels[zmonDurationLabelKey]; ok {
|
||||
duration, err = time.ParseDuration(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// parse tags
|
||||
tags := make(map[string]string)
|
||||
for k, v := range config.Labels {
|
||||
if strings.HasPrefix(k, zmonTagPrefixLabelKey) {
|
||||
key := strings.TrimPrefix(k, zmonTagPrefixLabelKey)
|
||||
tags[key] = v
|
||||
}
|
||||
}
|
||||
|
||||
// parse tags from annotations
|
||||
// tags defined in annotations takes precedence over tags defined in
|
||||
// the labels.
|
||||
for k, v := range annotations {
|
||||
if strings.HasPrefix(k, zmonTagPrefixAnnotationKey) {
|
||||
key := strings.TrimPrefix(k, zmonTagPrefixAnnotationKey)
|
||||
tags[key] = v
|
||||
}
|
||||
}
|
||||
|
||||
// default aggregator is last
|
||||
aggregators := []string{"last"}
|
||||
if k, ok := config.Labels[zmonAggregatorsLabelKey]; ok {
|
||||
aggregators = strings.Split(k, ",")
|
||||
}
|
||||
|
||||
return &ZMONCollector{
|
||||
zmon: zmon,
|
||||
interval: interval,
|
||||
checkID: checkID,
|
||||
key: key,
|
||||
tags: tags,
|
||||
duration: duration,
|
||||
aggregators: aggregators,
|
||||
metricName: config.Name,
|
||||
metricType: config.Type,
|
||||
labels: config.Labels,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetMetrics returns a list of collected metrics for the ZMON check.
|
||||
func (c *ZMONCollector) GetMetrics() ([]CollectedMetric, error) {
|
||||
dataPoints, err := c.zmon.Query(c.checkID, c.key, c.tags, c.aggregators, c.duration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(dataPoints) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// pick the last data point
|
||||
// TODO: do more fancy aggregations here (or in the query function)
|
||||
point := dataPoints[len(dataPoints)-1]
|
||||
|
||||
metricValue := CollectedMetric{
|
||||
Type: c.metricType,
|
||||
External: external_metrics.ExternalMetricValue{
|
||||
MetricName: c.metricName,
|
||||
MetricLabels: c.labels,
|
||||
Timestamp: metav1.Time{Time: point.Time},
|
||||
Value: *resource.NewMilliQuantity(int64(point.Value*1000), resource.DecimalSI),
|
||||
},
|
||||
}
|
||||
|
||||
return []CollectedMetric{metricValue}, nil
|
||||
}
|
||||
|
||||
// Interval returns the interval at which the collector should run.
|
||||
func (c *ZMONCollector) Interval() time.Duration {
|
||||
return c.interval
|
||||
}
|
140
pkg/collector/zmon_collector_test.go
Normal file
140
pkg/collector/zmon_collector_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zalando-incubator/kube-metrics-adapter/pkg/zmon"
|
||||
autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/metrics/pkg/apis/external_metrics"
|
||||
)
|
||||
|
||||
type zmonMock struct {
|
||||
dataPoints []zmon.DataPoint
|
||||
entities []zmon.Entity
|
||||
}
|
||||
|
||||
func (m zmonMock) Query(checkID int, key string, tags map[string]string, aggregators []string, duration time.Duration) ([]zmon.DataPoint, error) {
|
||||
return m.dataPoints, nil
|
||||
}
|
||||
|
||||
func TestZMONCollectorNewCollector(t *testing.T) {
|
||||
collectPlugin, _ := NewZMONCollectorPlugin(zmonMock{})
|
||||
|
||||
config := &MetricConfig{
|
||||
MetricTypeName: MetricTypeName{
|
||||
Name: ZMONCheckMetric,
|
||||
},
|
||||
Labels: map[string]string{
|
||||
zmonCheckIDLabelKey: "1234",
|
||||
zmonAggregatorsLabelKey: "max",
|
||||
zmonTagPrefixLabelKey + "alias": "cluster_alias",
|
||||
zmonDurationLabelKey: "5m",
|
||||
zmonKeyLabelKey: "key",
|
||||
},
|
||||
}
|
||||
|
||||
hpa := &autoscalingv2beta1.HorizontalPodAutoscaler{}
|
||||
|
||||
collector, err := collectPlugin.NewCollector(hpa, config, 1*time.Second)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, collector)
|
||||
zmonCollector := collector.(*ZMONCollector)
|
||||
require.Equal(t, "key", zmonCollector.key)
|
||||
require.Equal(t, 1234, zmonCollector.checkID)
|
||||
require.Equal(t, 1*time.Second, zmonCollector.interval)
|
||||
require.Equal(t, 5*time.Minute, zmonCollector.duration)
|
||||
require.Equal(t, []string{"max"}, zmonCollector.aggregators)
|
||||
require.Equal(t, map[string]string{"alias": "cluster_alias"}, zmonCollector.tags)
|
||||
|
||||
// check that annotations overwrites labels
|
||||
hpa.ObjectMeta = metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
zmonKeyAnnotationKey: "annotation_key",
|
||||
zmonTagPrefixAnnotationKey + "alias": "cluster_alias_annotation",
|
||||
},
|
||||
}
|
||||
collector, err = collectPlugin.NewCollector(hpa, config, 1*time.Second)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, collector)
|
||||
zmonCollector = collector.(*ZMONCollector)
|
||||
require.Equal(t, "annotation_key", zmonCollector.key)
|
||||
require.Equal(t, map[string]string{"alias": "cluster_alias_annotation"}, zmonCollector.tags)
|
||||
|
||||
// should fail if the metric name isn't ZMON
|
||||
config.Name = "non-zmon-check"
|
||||
_, err = collectPlugin.NewCollector(nil, config, 1*time.Second)
|
||||
require.Error(t, err)
|
||||
|
||||
// should fail if the check id is not specified.
|
||||
delete(config.Labels, zmonCheckIDLabelKey)
|
||||
config.Name = ZMONCheckMetric
|
||||
_, err = collectPlugin.NewCollector(nil, config, 1*time.Second)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestZMONCollectorGetMetrics(tt *testing.T) {
|
||||
config := &MetricConfig{
|
||||
MetricTypeName: MetricTypeName{
|
||||
Name: ZMONCheckMetric,
|
||||
Type: "foo",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
zmonCheckIDLabelKey: "1234",
|
||||
zmonAggregatorsLabelKey: "max",
|
||||
zmonTagPrefixLabelKey + "alias": "cluster_alias",
|
||||
zmonDurationLabelKey: "5m",
|
||||
zmonKeyLabelKey: "key",
|
||||
},
|
||||
}
|
||||
|
||||
for _, ti := range []struct {
|
||||
msg string
|
||||
dataPoints []zmon.DataPoint
|
||||
collectedMetrics []CollectedMetric
|
||||
}{
|
||||
{
|
||||
msg: "test successfully getting metrics",
|
||||
dataPoints: []zmon.DataPoint{
|
||||
{
|
||||
Time: time.Time{},
|
||||
Value: 1.0,
|
||||
},
|
||||
},
|
||||
collectedMetrics: []CollectedMetric{
|
||||
{
|
||||
Type: config.Type,
|
||||
External: external_metrics.ExternalMetricValue{
|
||||
MetricName: config.Name,
|
||||
MetricLabels: config.Labels,
|
||||
Timestamp: metav1.Time{Time: time.Time{}},
|
||||
Value: *resource.NewMilliQuantity(int64(1.0)*1000, resource.DecimalSI),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
msg: "test not getting any metrics",
|
||||
},
|
||||
} {
|
||||
tt.Run(ti.msg, func(t *testing.T) {
|
||||
z := zmonMock{
|
||||
dataPoints: ti.dataPoints,
|
||||
}
|
||||
|
||||
zmonCollector, err := NewZMONCollector(z, config, nil, 1*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
metrics, _ := zmonCollector.GetMetrics()
|
||||
require.Equal(t, ti.collectedMetrics, metrics)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestZMONCollectorInterval(t *testing.T) {
|
||||
collector := ZMONCollector{interval: 1 * time.Second}
|
||||
require.Equal(t, 1*time.Second, collector.Interval())
|
||||
}
|
Reference in New Issue
Block a user