Skip to content

Commit

Permalink
Added support for proxy and devapp monitored resources (#189)
Browse files Browse the repository at this point in the history
Implements matching of resource type based on metric labels.
  • Loading branch information
jkohen authored Oct 21, 2019
1 parent ee77fae commit 65ff039
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 51 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/gogo/protobuf v1.2.2-0.20190730201129-28a6bbf47e48 // indirect
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect
github.com/golang/protobuf v1.3.2
github.com/google/go-cmp v0.3.0
github.com/googleapis/gnostic v0.3.0 // indirect
github.com/gophercloud/gophercloud v0.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
Expand Down
74 changes: 61 additions & 13 deletions retrieval/resource_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ func constValue(labelName string) labelTranslation {
type ResourceMap struct {
// The name of the Stackdriver MonitoredResource.
Type string
// MatchLabel must exist in the set of Prometheus labels in order for this map to match. Ignored if empty.
MatchLabel string
// Mapping from Prometheus to Stackdriver labels
LabelMap map[string]labelTranslation
}
Expand Down Expand Up @@ -88,8 +90,37 @@ var GKEResourceMap = ResourceMap{
},
}

// TODO(jkohen): ensure these are sorted from more specific to less specific.
var ResourceMappings = []ResourceMap{
var DevappResourceMap = ResourceMap{
Type: "devapp",
MatchLabel: "__meta_kubernetes_pod_label_type_devapp",
LabelMap: map[string]labelTranslation{
ProjectIDLabel: constValue("resource_container"),
KubernetesLocationLabel: constValue("location"),
"__meta_kubernetes_pod_label_org": constValue("org"),
"__meta_kubernetes_pod_label_env": constValue("env"),
"api_product_name": constValue("api_product_name"),
},
}

var ProxyResourceMap = ResourceMap{
Type: "proxy",
MatchLabel: "__meta_kubernetes_pod_label_type_proxy",
LabelMap: map[string]labelTranslation{
ProjectIDLabel: constValue("resource_container"),
KubernetesLocationLabel: constValue("location"),
"__meta_kubernetes_pod_label_org": constValue("org"),
"__meta_kubernetes_pod_label_env": constValue("env"),
"proxy_name": constValue("proxy_name"),
"revision": constValue("revision"),
},
}

type ResourceMapList []ResourceMap

// When you add new elements, you also probably want to update TestResourceMappingsOrder.
var ResourceMappings = ResourceMapList{
ProxyResourceMap,
DevappResourceMap,
{
Type: "k8s_container",
LabelMap: map[string]labelTranslation{
Expand Down Expand Up @@ -134,40 +165,57 @@ var ResourceMappings = []ResourceMap{
},
}

func (m *ResourceMap) Translate(discovered, final labels.Labels) map[string]string {
stackdriverLabels := m.tryTranslate(discovered, final)
// Translate translates labels to a monitored resource and entry labels, if
// possible. Returns the resource and the modified entry labels.
//
// The labels in `discovered` and `entryLabels` are used as input. If a label
// exists in both sets, the one in `entryLabels` takes precedence. Whenever a
// label from `entryLabels` is used, it is removed from the set that is
// returned.
func (m *ResourceMap) Translate(discovered, entryLabels labels.Labels) (map[string]string, labels.Labels) {
stackdriverLabels, entryLabels := m.tryTranslate(discovered, entryLabels)
if len(m.LabelMap) == len(stackdriverLabels) {
return stackdriverLabels
return stackdriverLabels, entryLabels
}
return nil
return nil, nil
}

// BestEffortTranslate translates labels to resource with best effort. If the resource label
// cannot be filled, use empty string instead.
func (m *ResourceMap) BestEffortTranslate(discovered, final labels.Labels) map[string]string {
stackdriverLabels := m.tryTranslate(discovered, final)
func (m *ResourceMap) BestEffortTranslate(discovered, entryLabels labels.Labels) (map[string]string, labels.Labels) {
stackdriverLabels, entryLabels := m.tryTranslate(discovered, entryLabels)
for _, t := range m.LabelMap {
if _, ok := stackdriverLabels[t.stackdriverLabelName]; !ok {
stackdriverLabels[t.stackdriverLabelName] = ""
}
}
return stackdriverLabels
return stackdriverLabels, entryLabels
}

func (m *ResourceMap) tryTranslate(discovered, final labels.Labels) map[string]string {
func (m *ResourceMap) tryTranslate(discovered, entryLabels labels.Labels) (map[string]string, labels.Labels) {
matched := false
stackdriverLabels := make(map[string]string, len(m.LabelMap))
for _, l := range discovered {
if l.Name == m.MatchLabel {
matched = true
}
if translator, ok := m.LabelMap[l.Name]; ok {
stackdriverLabels[translator.stackdriverLabelName] = translator.convert(l.Value)
}
}
// The final labels are applied second so they overwrite mappings from discovered labels.
// The entryLabels labels are applied second so they overwrite mappings from discovered labels.
// This ensures, that the Prometheus's relabeling rules are respected for labels that
// appear in both label sets, e.g. the "job" label for generic resources.
for _, l := range final {
var finalLabels labels.Labels
for _, l := range entryLabels {
if translator, ok := m.LabelMap[l.Name]; ok {
stackdriverLabels[translator.stackdriverLabelName] = translator.convert(l.Value)
} else {
finalLabels = append(finalLabels, l)
}
}
return stackdriverLabels
if len(m.MatchLabel) > 0 && !matched {
return nil, finalLabels
}
return stackdriverLabels, finalLabels
}
166 changes: 159 additions & 7 deletions retrieval/resource_map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import (
"reflect"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/prometheus/prometheus/pkg/labels"
)

func TestTranslate(t *testing.T) {
r := ResourceMap{
Type: "my_type",
Type: "my_type",
MatchLabel: "__match_type",
LabelMap: map[string]labelTranslation{
"__target1": constValue("sdt1"),
"__target2": constValue("sdt2"),
Expand All @@ -33,29 +35,40 @@ func TestTranslate(t *testing.T) {
noMatchTarget := labels.Labels{
{"ignored", "x"},
{"__target2", "y"},
{"__match_type", "true"},
}
if labels := r.Translate(noMatchTarget, nil); labels != nil {
if labels, _ := r.Translate(noMatchTarget, nil); labels != nil {
t.Errorf("Expected no match, matched %v", labels)
}
matchTargetDiscovered := labels.Labels{
{"ignored", "x"},
{"__target2", "y"},
{"__target1", "z"},
{"__match_type", "true"},
}
matchTargetFinal := labels.Labels{
{"__target1", "z2"},
{"__target3", "v"},
{"__match_type", "true"},
}
expectedLabels := map[string]string{
"sdt1": "z2",
"sdt2": "y",
"sdt3": "v",
}
if labels := r.Translate(matchTargetDiscovered, matchTargetFinal); labels == nil {
if labels, _ := r.Translate(matchTargetDiscovered, matchTargetFinal); labels == nil {
t.Errorf("Expected %v, actual nil", expectedLabels)
} else if !reflect.DeepEqual(labels, expectedLabels) {
t.Errorf("Expected %v, actual %v", expectedLabels, labels)
}
missingType := labels.Labels{
{"__target1", "x"},
{"__target2", "y"},
{"__target3", "z"},
}
if labels, _ := r.Translate(missingType, nil); labels != nil {
t.Errorf("Expected no match, matched %v", labels)
}
}

func TestTranslateEc2Instance(t *testing.T) {
Expand All @@ -71,7 +84,7 @@ func TestTranslateEc2Instance(t *testing.T) {
"region": "aws:us-east-1b",
"aws_account": "12345678",
}
if labels := EC2ResourceMap.Translate(target, nil); labels == nil {
if labels, _ := EC2ResourceMap.Translate(target, nil); labels == nil {
t.Errorf("Expected %v, actual nil", expectedLabels)
} else if !reflect.DeepEqual(labels, expectedLabels) {
t.Errorf("Expected %v, actual %v", expectedLabels, labels)
Expand All @@ -89,7 +102,7 @@ func TestTranslateGceInstance(t *testing.T) {
"zone": "us-central1-a",
"instance_id": "1234110975759588",
}
if labels := GCEResourceMap.Translate(target, nil); labels == nil {
if labels, _ := GCEResourceMap.Translate(target, nil); labels == nil {
t.Errorf("Expected %v, actual nil", expectedLabels)
} else if !reflect.DeepEqual(labels, expectedLabels) {
t.Errorf("Expected %v, actual %v", expectedLabels, labels)
Expand All @@ -111,13 +124,152 @@ func TestBestEffortTranslate(t *testing.T) {
"pod_id": "",
"container_name": "",
}
if labels := GKEResourceMap.BestEffortTranslate(target, nil); labels == nil {
if labels, _ := GKEResourceMap.BestEffortTranslate(target, nil); labels == nil {
t.Errorf("Expected %v, actual nil", expectedLabels)
} else if !reflect.DeepEqual(labels, expectedLabels) {
t.Errorf("Expected %v, actual %v", expectedLabels, labels)
}
}

func TestTranslateDevapp(t *testing.T) {
discoveredLabels := labels.Labels{
{"__meta_kubernetes_pod_label_type_devapp", "true"},
{ProjectIDLabel, "my-project"},
{KubernetesLocationLabel, "us-central1-a"},
{"__meta_kubernetes_pod_label_org", "my-org"},
{"__meta_kubernetes_pod_label_env", "my-env"},
}
metricLabels := labels.Labels{
{"api_product_name", "my-name"},
{"extra_label", "my-label"},
}
expectedLabels := map[string]string{
"resource_container": "my-project",
"location": "us-central1-a",
"org": "my-org",
"env": "my-env",
"api_product_name": "my-name",
}
expectedFinalLabels := labels.Labels{
{"extra_label", "my-label"},
}
if labels, finalLabels := DevappResourceMap.Translate(discoveredLabels, metricLabels); labels == nil {
t.Errorf("Expected %v, actual nil", expectedLabels)
} else {
if diff := cmp.Diff(expectedLabels, labels); len(diff) > 0 {
t.Error(diff)
}
if diff := cmp.Diff(expectedFinalLabels, finalLabels); len(diff) > 0 {
t.Error(diff)
}
}
}

func TestTranslateProxy(t *testing.T) {
discoveredLabels := labels.Labels{
{"__meta_kubernetes_pod_label_type_proxy", "true"},
{ProjectIDLabel, "my-project"},
{KubernetesLocationLabel, "us-central1-a"},
{"__meta_kubernetes_pod_label_org", "my-org"},
{"__meta_kubernetes_pod_label_env", "my-env"},
}
metricLabels := labels.Labels{
{"proxy_name", "my-name"},
{"revision", "my-revision"},
{"extra_label", "my-label"},
}
expectedLabels := map[string]string{
"resource_container": "my-project",
"location": "us-central1-a",
"org": "my-org",
"env": "my-env",
"proxy_name": "my-name",
"revision": "my-revision",
}
expectedFinalLabels := labels.Labels{
{"extra_label", "my-label"},
}
if labels, finalLabels := ProxyResourceMap.Translate(discoveredLabels, metricLabels); labels == nil {
t.Errorf("Expected %v, actual nil", expectedLabels)
} else {
if diff := cmp.Diff(expectedLabels, labels); len(diff) > 0 {
t.Error(diff)
}
if diff := cmp.Diff(expectedFinalLabels, finalLabels); len(diff) > 0 {
t.Error(diff)
}
}
}

func (m *ResourceMapList) getByType(t string) (*ResourceMap, bool) {
for _, m := range *m {
if m.Type == t {
return &m, true
}
}
return nil, false
}

func (m *ResourceMapList) matchType(matchLabels labels.Labels) string {
for _, m := range *m {
if lset, _ := m.Translate(matchLabels, nil); lset != nil {
return m.Type
}
}
return ""
}

func TestResourceMappingsOrder(t *testing.T) {
// For each pair of resource types on the input, ensure that the first
// one is picked if there are labels that match both. This guarantees
// that more specific resource types are picked, e.g. k8s_container before
// k8s_pod, and k8s_node before gce_instance.
cases := []struct {
first string // Higher priority.
second string // Lower priority.
}{
{"k8s_container", "k8s_pod"},
{"k8s_pod", "k8s_node"},
{"k8s_node", "gce_instance"},
{"k8s_node", "aws_ec2_instance"},
{"proxy", "k8s_container"},
{"devapp", "k8s_container"},
}
for _, c := range cases {
var (
first, second *ResourceMap
ok bool
)
if first, ok = ResourceMappings.getByType(c.first); !ok {
t.Fatalf("invalid test case, missing %v", c.first)
}
if second, ok = ResourceMappings.getByType(c.second); !ok {
t.Fatalf("invalid test case, missing %v", c.second)
}
// The values are uninteresting for this test.
combinedKeys := make(map[string]string)
for k, _ := range first.LabelMap {
combinedKeys[k] = ""
}
if len(first.MatchLabel) > 0 {
combinedKeys[first.MatchLabel] = ""
}
for k, _ := range second.LabelMap {
combinedKeys[k] = ""
}
if len(second.MatchLabel) > 0 {
combinedKeys[second.MatchLabel] = ""
}
combinedLabels := labels.FromMap(combinedKeys)
if match := ResourceMappings.matchType(combinedLabels); match != c.first {
t.Errorf("expected to match %v, got %v", c.first, match)
}
if match := ResourceMappings.matchType(combinedLabels); match == c.second {
t.Errorf("unexpected match %v", match)
}
}
}

func BenchmarkTranslate(b *testing.B) {
r := ResourceMap{
Type: "gke_container",
Expand Down Expand Up @@ -149,7 +301,7 @@ func BenchmarkTranslate(b *testing.B) {
b.ReportAllocs()

for i := 0; i < b.N; i++ {
if labels := r.Translate(discoveredLabels, finalLabels); labels == nil {
if labels, _ := r.Translate(discoveredLabels, finalLabels); labels == nil {
b.Fail()
}
}
Expand Down
Loading

0 comments on commit 65ff039

Please sign in to comment.