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:
Mikkel Oscar Lyderik Larsen
2018-10-29 14:26:25 +01:00
committed by Arjun
parent b18acf3ed0
commit c86a82ca88
8 changed files with 964 additions and 31 deletions

View 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
}

View 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())
}