From 37df97c6c4fc55660e0445700e94c571c92e7d6f Mon Sep 17 00:00:00 2001
From: Filippo Giunchedi <filippo@wikimedia.org>
Date: Fri, 27 Aug 2021 14:49:55 +0200
Subject: [PATCH] Add mmkubernetes support

---
 exporter.go        |   9 ++++
 kubernetes.go      | 125 +++++++++++++++++++++++++++++++++++++++++++++
 kubernetes_test.go | 121 +++++++++++++++++++++++++++++++++++++++++++
 utils.go           |   2 +
 4 files changed, 257 insertions(+)
 create mode 100644 kubernetes.go
 create mode 100644 kubernetes_test.go

diff --git a/exporter.go b/exporter.go
index c2a0630..eb4faad 100644
--- a/exporter.go
+++ b/exporter.go
@@ -23,6 +23,7 @@ const (
 	rsyslogDynafileCache
 	rsyslogInputIMDUP
 	rsyslogForward
+	rsyslogKubernetes
 )
 
 type rsyslogExporter struct {
@@ -121,6 +122,14 @@ func (re *rsyslogExporter) handleStatLine(rawbuf []byte) error {
 		for _, p := range f.toPoints() {
 			re.set(p)
 		}
+	case rsyslogKubernetes:
+		k, err := newKubernetesFromJSON(buf)
+		if err != nil {
+			return err
+		}
+		for _, p := range k.toPoints() {
+			re.set(p)
+		}
 
 	default:
 		return fmt.Errorf("unknown pstat type: %v", pstatType)
diff --git a/kubernetes.go b/kubernetes.go
new file mode 100644
index 0000000..8ce43ca
--- /dev/null
+++ b/kubernetes.go
@@ -0,0 +1,125 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"regexp"
+)
+
+var (
+	apiNameRegexp = regexp.MustCompile(`mmkubernetes\((\S+)\)`)
+)
+
+type kubernetes struct {
+	Name                  string `json:"name"`
+	Url                   string
+	RecordSeen            int64 `json:"recordseen"`
+	NamespaceMetaSuccess  int64 `json:"namespacemetadatasuccess"`
+	NamespaceMetaNotFound int64 `json:"namespacemetadatanotfound"`
+	NamespaceMetaBusy     int64 `json:"namespacemetadatabusy"`
+	NamespaceMetaError    int64 `json:"namespacemetadataerror"`
+	PodMetaSuccess        int64 `json:"podmetadatasuccess"`
+	PodMetaNotFound       int64 `json:"podmetadatanotfound"`
+	PodMetaBusy           int64 `json:"podmetadatabusy"`
+	PodMetaError          int64 `json:"podmetadataerror"`
+}
+
+func newKubernetesFromJSON(b []byte) (*kubernetes, error) {
+	var pstat kubernetes
+	err := json.Unmarshal(b, &pstat)
+	if err != nil {
+		return nil, fmt.Errorf("failed to decode kubernetes stat `%v`: %v", string(b), err)
+	}
+	matches := apiNameRegexp.FindSubmatch([]byte(pstat.Name))
+	if matches != nil {
+		pstat.Url = string(matches[1])
+	}
+	return &pstat, nil
+}
+
+func (k *kubernetes) toPoints() []*point {
+	points := make([]*point, 9)
+
+	points[0] = &point{
+		Name:        "kubernetes_namespace_metadata_success_total",
+		Type:        counter,
+		Value:       k.NamespaceMetaSuccess,
+		Description: "successful fetches of namespace metadata",
+		LabelName:   "url",
+		LabelValue:  k.Url,
+	}
+
+	points[1] = &point{
+		Name:        "kubernetes_namespace_metadata_notfound_total",
+		Type:        counter,
+		Value:       k.NamespaceMetaNotFound,
+		Description: "notfound fetches of namespace metadata",
+		LabelName:   "url",
+		LabelValue:  k.Url,
+	}
+
+	points[2] = &point{
+		Name:        "kubernetes_namespace_metadata_busy_total",
+		Type:        counter,
+		Value:       k.NamespaceMetaBusy,
+		Description: "busy fetches of namespace metadata",
+		LabelName:   "url",
+		LabelValue:  k.Url,
+	}
+
+	points[3] = &point{
+		Name:        "kubernetes_namespace_metadata_error_total",
+		Type:        counter,
+		Value:       k.NamespaceMetaError,
+		Description: "error fetches of namespace metadata",
+		LabelName:   "url",
+		LabelValue:  k.Url,
+	}
+
+	points[4] = &point{
+		Name:        "kubernetes_pod_metadata_success_total",
+		Type:        counter,
+		Value:       k.PodMetaSuccess,
+		Description: "successful fetches of pod metadata",
+		LabelName:   "url",
+		LabelValue:  k.Url,
+	}
+
+	points[5] = &point{
+		Name:        "kubernetes_pod_metadata_notfound_total",
+		Type:        counter,
+		Value:       k.PodMetaNotFound,
+		Description: "notfound fetches of pod metadata",
+		LabelName:   "url",
+		LabelValue:  k.Url,
+	}
+
+	points[6] = &point{
+		Name:        "kubernetes_pod_metadata_busy_total",
+		Type:        counter,
+		Value:       k.PodMetaBusy,
+		Description: "busy fetches of pod metadata",
+		LabelName:   "url",
+		LabelValue:  k.Url,
+	}
+
+	points[7] = &point{
+		Name:        "kubernetes_pod_metadata_error_total",
+		Type:        counter,
+		Value:       k.PodMetaError,
+		Description: "error fetches of pod metadata",
+		LabelName:   "url",
+		LabelValue:  k.Url,
+	}
+
+	points[8] = &point{
+		Name:        "kubernetes_record_seen_total",
+		Type:        counter,
+		Value:       k.RecordSeen,
+		Description: "records fetched from the api",
+		LabelName:   "url",
+		LabelValue:  k.Url,
+	}
+
+	return points
+}
diff --git a/kubernetes_test.go b/kubernetes_test.go
new file mode 100644
index 0000000..6054b60
--- /dev/null
+++ b/kubernetes_test.go
@@ -0,0 +1,121 @@
+package main
+
+import "testing"
+
+var (
+	kubernetesLog = []byte(`{ "name": "mmkubernetes(https://host.domain.tld:6443)", "origin": "mmkubernetes", "recordseen": 477943, "namespacemetadatasuccess": 7, "namespacemetadatanotfound": 0, "namespacemetadatabusy": 0, "namespacemetadataerror": 0, "podmetadatasuccess": 26, "podmetadatanotfound": 0, "podmetadatabusy": 0, "podmetadataerror": 0 }`)
+)
+
+func TestNewKubernetesFromJSON(t *testing.T) {
+	logType := getStatType(kubernetesLog)
+	if logType != rsyslogKubernetes {
+		t.Errorf("detected pstat type should be %d but is %d", rsyslogKubernetes, logType)
+	}
+
+	pstat, err := newKubernetesFromJSON([]byte(kubernetesLog))
+	if err != nil {
+		t.Fatalf("expected parsing action not to fail, got: %v", err)
+	}
+
+	if want, got := "mmkubernetes(https://host.domain.tld:6443)", pstat.Name; want != got {
+		t.Errorf("wanted '%s', got '%s'", want, got)
+	}
+
+	if want, got := "https://host.domain.tld:6443", pstat.Url; want != got {
+		t.Errorf("wanted '%s', got '%s'", want, got)
+	}
+
+	if want, got := int64(477943), pstat.RecordSeen; want != got {
+		t.Errorf("wanted '%d', got '%d'", want, got)
+	}
+
+	if want, got := int64(7), pstat.NamespaceMetaSuccess; want != got {
+		t.Errorf("wanted '%d', got '%d'", want, got)
+	}
+
+	if want, got := int64(0), pstat.NamespaceMetaNotFound; want != got {
+		t.Errorf("wanted '%d', got '%d'", want, got)
+	}
+
+	if want, got := int64(0), pstat.NamespaceMetaBusy; want != got {
+		t.Errorf("wanted '%d', got '%d'", want, got)
+	}
+
+	if want, got := int64(0), pstat.NamespaceMetaError; want != got {
+		t.Errorf("wanted '%d', got '%d'", want, got)
+	}
+
+	if want, got := int64(26), pstat.PodMetaSuccess; want != got {
+		t.Errorf("wanted '%d', got '%d'", want, got)
+	}
+
+	if want, got := int64(0), pstat.PodMetaNotFound; want != got {
+		t.Errorf("wanted '%d', got '%d'", want, got)
+	}
+
+	if want, got := int64(0), pstat.PodMetaBusy; want != got {
+		t.Errorf("wanted '%d', got '%d'", want, got)
+	}
+
+	if want, got := int64(0), pstat.PodMetaError; want != got {
+		t.Errorf("wanted '%d', got '%d'", want, got)
+	}
+
+}
+
+func TestKubernetesToPoints(t *testing.T) {
+	pstat, err := newKubernetesFromJSON([]byte(kubernetesLog))
+	if err != nil {
+		t.Fatalf("expected parsing action not to fail, got: %v", err)
+	}
+	points := pstat.toPoints()
+
+	point := points[0]
+	if want, got := "kubernetes_namespace_metadata_success_total", point.Name; want != got {
+		t.Errorf("wanted '%s', got '%s'", want, got)
+	}
+
+	if want, got := "https://host.domain.tld:6443", point.LabelValue; want != got {
+		t.Errorf("wanted '%s', got '%s'", want, got)
+	}
+
+	point = points[1]
+	if want, got := "kubernetes_namespace_metadata_notfound_total", point.Name; want != got {
+		t.Errorf("wanted '%s', got '%s'", want, got)
+	}
+
+	point = points[2]
+	if want, got := "kubernetes_namespace_metadata_busy_total", point.Name; want != got {
+		t.Errorf("wanted '%s', got '%s'", want, got)
+	}
+
+	point = points[3]
+	if want, got := "kubernetes_namespace_metadata_error_total", point.Name; want != got {
+		t.Errorf("wanted '%s', got '%s'", want, got)
+	}
+
+	point = points[4]
+	if want, got := "kubernetes_pod_metadata_success_total", point.Name; want != got {
+		t.Errorf("wanted '%s', got '%s'", want, got)
+	}
+
+	point = points[5]
+	if want, got := "kubernetes_pod_metadata_notfound_total", point.Name; want != got {
+		t.Errorf("wanted '%s', got '%s'", want, got)
+	}
+
+	point = points[6]
+	if want, got := "kubernetes_pod_metadata_busy_total", point.Name; want != got {
+		t.Errorf("wanted '%s', got '%s'", want, got)
+	}
+
+	point = points[7]
+	if want, got := "kubernetes_pod_metadata_error_total", point.Name; want != got {
+		t.Errorf("wanted '%s', got '%s'", want, got)
+	}
+
+	point = points[8]
+	if want, got := "kubernetes_record_seen_total", point.Name; want != got {
+		t.Errorf("wanted '%s', got '%s'", want, got)
+	}
+}
diff --git a/utils.go b/utils.go
index 624fd28..3bbaa4a 100644
--- a/utils.go
+++ b/utils.go
@@ -20,6 +20,8 @@ func getStatType(buf []byte) rsyslogType {
 		return rsyslogDynafileCache
 	} else if strings.Contains(line, "omfwd") {
 		return rsyslogForward
+	} else if strings.Contains(line, "mmkubernetes") {
+		return rsyslogKubernetes
 	}
 	return rsyslogUnknown
 }
-- 
GitLab