diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 531eb6c7df97..9efd7ffec920 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -204,6 +204,7 @@ https://github.com/elastic/beats/compare/v6.0.0-beta2...master[Check the HEAD di - Add experimental uwsgi module. {pull}6006[6006] - Docker and Kubernetes modules are now GA, instead of Beta. {pull}6105[6105] - Add pct calculated fields for Pod and container CPU and memory usages. {pull}6158[6158] +- Add statefulset support to Kubernetes module. {pull}6236[6236] *Packetbeat* diff --git a/metricbeat/docs/fields.asciidoc b/metricbeat/docs/fields.asciidoc index 4725ddb8535e..efeb701bf70e 100644 --- a/metricbeat/docs/fields.asciidoc +++ b/metricbeat/docs/fields.asciidoc @@ -6151,6 +6151,75 @@ type: long The number of fully labeled replicas per ReplicaSet +[float] +== statefulset fields + +kubernetes stateful set metrics + + + +[float] +=== `kubernetes.statefulset.name` + +type: keyword + +Kubernetes stateful set name + + +[float] +=== `kubernetes.statefulset.created` + +type: long + +The creation timestamp (epoch) for StatefulSet + + +[float] +== replicas fields + +Kubernetes stateful set replicas status + + + +[float] +=== `kubernetes.statefulset.replicas.observed` + +type: long + +The number of observed replicas per StatefulSet + + +[float] +=== `kubernetes.statefulset.replicas.desired` + +type: long + +The number of desired replicas per StatefulSet + + +[float] +== generation fields + +Kubernetes stateful set generation information + + + +[float] +=== `kubernetes.statefulset.generation.observed` + +type: long + +The observed generation per StatefulSet + + +[float] +=== `kubernetes.statefulset.generation.desired` + +type: long + +The desired generation per StatefulSet + + [float] == system fields diff --git a/metricbeat/docs/modules/kubernetes.asciidoc b/metricbeat/docs/modules/kubernetes.asciidoc index 043adbd73846..51c8a7f8f637 100644 --- a/metricbeat/docs/modules/kubernetes.asciidoc +++ b/metricbeat/docs/modules/kubernetes.asciidoc @@ -40,6 +40,7 @@ metricbeat.modules: - state_node - state_deployment - state_replicaset + - state_statefulset - state_pod - state_container period: 10s @@ -77,6 +78,8 @@ The following metricsets are available: * <> +* <> + * <> * <> @@ -99,6 +102,8 @@ include::kubernetes/state_pod.asciidoc[] include::kubernetes/state_replicaset.asciidoc[] +include::kubernetes/state_statefulset.asciidoc[] + include::kubernetes/system.asciidoc[] include::kubernetes/volume.asciidoc[] diff --git a/metricbeat/docs/modules/kubernetes/state_statefulset.asciidoc b/metricbeat/docs/modules/kubernetes/state_statefulset.asciidoc new file mode 100644 index 000000000000..b53d50de6ba9 --- /dev/null +++ b/metricbeat/docs/modules/kubernetes/state_statefulset.asciidoc @@ -0,0 +1,21 @@ +//// +This file is generated! See scripts/docs_collector.py +//// + +[[metricbeat-metricset-kubernetes-state_statefulset]] +=== Kubernetes state_statefulset metricset + +include::../../../module/kubernetes/state_statefulset/_meta/docs.asciidoc[] + + +==== Fields + +For a description of each field in the metricset, see the +<> section. + +Here is an example document generated by this metricset: + +[source,json] +---- +include::../../../module/kubernetes/state_statefulset/_meta/data.json[] +---- diff --git a/metricbeat/docs/modules_list.asciidoc b/metricbeat/docs/modules_list.asciidoc index cb5ade37ddc3..b29bc197de03 100644 --- a/metricbeat/docs/modules_list.asciidoc +++ b/metricbeat/docs/modules_list.asciidoc @@ -58,7 +58,7 @@ This file is generated! See scripts/docs_collector.py |<> beta[] | .1+| |<> beta[] |<> | -.11+| |<> +.12+| |<> |<> experimental[] |<> |<> @@ -67,6 +67,7 @@ This file is generated! See scripts/docs_collector.py |<> |<> |<> +|<> |<> |<> |<> experimental[] | diff --git a/metricbeat/include/list.go b/metricbeat/include/list.go index 094e2a4e6440..1e8068d59c2b 100644 --- a/metricbeat/include/list.go +++ b/metricbeat/include/list.go @@ -70,6 +70,7 @@ import ( _ "github.com/elastic/beats/metricbeat/module/kubernetes/state_node" _ "github.com/elastic/beats/metricbeat/module/kubernetes/state_pod" _ "github.com/elastic/beats/metricbeat/module/kubernetes/state_replicaset" + _ "github.com/elastic/beats/metricbeat/module/kubernetes/state_statefulset" _ "github.com/elastic/beats/metricbeat/module/kubernetes/system" _ "github.com/elastic/beats/metricbeat/module/kubernetes/util" _ "github.com/elastic/beats/metricbeat/module/kubernetes/volume" diff --git a/metricbeat/metricbeat.reference.yml b/metricbeat/metricbeat.reference.yml index dbf53567f86e..c8a610619d50 100644 --- a/metricbeat/metricbeat.reference.yml +++ b/metricbeat/metricbeat.reference.yml @@ -295,6 +295,7 @@ metricbeat.modules: - state_node - state_deployment - state_replicaset + - state_statefulset - state_pod - state_container period: 10s diff --git a/metricbeat/module/kubernetes/_meta/config.yml b/metricbeat/module/kubernetes/_meta/config.yml index cf00d10cd454..b15263cb7f4a 100644 --- a/metricbeat/module/kubernetes/_meta/config.yml +++ b/metricbeat/module/kubernetes/_meta/config.yml @@ -16,6 +16,7 @@ - state_node - state_deployment - state_replicaset + - state_statefulset - state_pod - state_container period: 10s diff --git a/metricbeat/module/kubernetes/_meta/test/kube-state-metrics b/metricbeat/module/kubernetes/_meta/test/kube-state-metrics index ec85a6b2d44b..4a1ba3005d30 100644 --- a/metricbeat/module/kubernetes/_meta/test/kube-state-metrics +++ b/metricbeat/module/kubernetes/_meta/test/kube-state-metrics @@ -386,3 +386,34 @@ process_start_time_seconds 1.4939719827e+09 # HELP process_virtual_memory_bytes Virtual memory size in bytes. # TYPE process_virtual_memory_bytes gauge process_virtual_memory_bytes 5.2932608e+07 +# HELP kube_statefulset_created Unix creation timestamp +# TYPE kube_statefulset_created gauge +kube_statefulset_created{namespace="default",statefulset="elasticsearch"} 1.511973651e+09 +kube_statefulset_created{namespace="default",statefulset="mysql"} 1.511989697e+09 +kube_statefulset_created{namespace="custom",statefulset="mysql"} 1.511999697e+09 +# HELP kube_statefulset_labels Kubernetes labels converted to Prometheus labels. +# TYPE kube_statefulset_labels gauge +kube_statefulset_labels{label_app="oci",label_io_kompose_service="elasticsearch",namespace="default",statefulset="elasticsearch"} 1 +kube_statefulset_labels{label_app="oci",label_custom_pod="true",label_io_kompose_service="s-mysql",namespace="default",statefulset="mysql"} 1 +kube_statefulset_labels{label_app="oci",label_custom_pod="true",label_io_kompose_service="s-mysql",namespace="custom",statefulset="mysql"} 1 +# HELP kube_statefulset_metadata_generation Sequence number representing a specific generation of the desired state for the StatefulSet. +# TYPE kube_statefulset_metadata_generation gauge +kube_statefulset_metadata_generation{namespace="default",statefulset="elasticsearch"} 3 +kube_statefulset_metadata_generation{namespace="default",statefulset="mysql"} 4 +kube_statefulset_metadata_generation{namespace="custom",statefulset="mysql"} 5 +# HELP kube_statefulset_replicas Number of desired pods for a StatefulSet. +# TYPE kube_statefulset_replicas gauge +kube_statefulset_replicas{namespace="default",statefulset="elasticsearch"} 4 +kube_statefulset_replicas{namespace="default",statefulset="mysql"} 5 +kube_statefulset_replicas{namespace="custom",statefulset="mysql"} 6 +# HELP kube_statefulset_status_observed_generation The generation observed by the StatefulSet controller. +# TYPE kube_statefulset_status_observed_generation gauge +kube_statefulset_status_observed_generation{namespace="default",statefulset="elasticsearch"} 1 +kube_statefulset_status_observed_generation{namespace="default",statefulset="mysql"} 2 +kube_statefulset_status_observed_generation{namespace="custom",statefulset="mysql"} 3 +# HELP kube_statefulset_status_replicas The number of replicas per StatefulSet. +# TYPE kube_statefulset_status_replicas gauge +kube_statefulset_status_replicas{namespace="default",statefulset="elasticsearch"} 1 +kube_statefulset_status_replicas{namespace="default",statefulset="mysql"} 2 +kube_statefulset_status_replicas{namespace="custom",statefulset="mysql"} 3 + diff --git a/metricbeat/module/kubernetes/state_statefulset/_meta/data.json b/metricbeat/module/kubernetes/state_statefulset/_meta/data.json new file mode 100644 index 000000000000..4cba9b597cf6 --- /dev/null +++ b/metricbeat/module/kubernetes/state_statefulset/_meta/data.json @@ -0,0 +1,30 @@ +{ + "@timestamp": "2017-05-10T16:46:37.821Z", + "beat": { + "hostname": "X1", + "name": "X1", + "version": "6.0.0-alpha1" + }, + "kubernetes": { + "namespace": "jenkins", + "statefulset": { + "name": "wise-lynx-jenkins-1616735317", + "created": 123454, + "replicas": { + "desired": 1, + "observed": 1, + }, + "generation": { + "desired": 1, + "observed": 1, + } + } + }, + "metricset": { + "host": "192.168.99.100:18080", + "module": "kubernetes", + "name": "state_statefulset", + "namespace": "statefulset", + "rtt": 6719 + } +} diff --git a/metricbeat/module/kubernetes/state_statefulset/_meta/docs.asciidoc b/metricbeat/module/kubernetes/state_statefulset/_meta/docs.asciidoc new file mode 100644 index 000000000000..d2f15f21749c --- /dev/null +++ b/metricbeat/module/kubernetes/state_statefulset/_meta/docs.asciidoc @@ -0,0 +1 @@ +This is the `state_statefulset` metricset of the Kubernetes module. diff --git a/metricbeat/module/kubernetes/state_statefulset/_meta/fields.yml b/metricbeat/module/kubernetes/state_statefulset/_meta/fields.yml new file mode 100644 index 000000000000..c524aa07b8f0 --- /dev/null +++ b/metricbeat/module/kubernetes/state_statefulset/_meta/fields.yml @@ -0,0 +1,40 @@ +- name: statefulset + type: group + description: > + kubernetes stateful set metrics + release: ga + fields: + - name: name + type: keyword + description: > + Kubernetes stateful set name + - name: created + type: long + description: > + The creation timestamp (epoch) for StatefulSet + - name: replicas + type: group + description: > + Kubernetes stateful set replicas status + fields: + - name: observed + type: long + description: > + The number of observed replicas per StatefulSet + - name: desired + type: long + description: > + The number of desired replicas per StatefulSet + - name: generation + type: group + description: > + Kubernetes stateful set generation information + fields: + - name: observed + type: long + description: > + The observed generation per StatefulSet + - name: desired + type: long + description: > + The desired generation per StatefulSet diff --git a/metricbeat/module/kubernetes/state_statefulset/data.go b/metricbeat/module/kubernetes/state_statefulset/data.go new file mode 100644 index 000000000000..aa0d38331fd9 --- /dev/null +++ b/metricbeat/module/kubernetes/state_statefulset/data.go @@ -0,0 +1,53 @@ +package state_statefulset + +import ( + "github.com/elastic/beats/libbeat/common" + "github.com/elastic/beats/metricbeat/mb" + "github.com/elastic/beats/metricbeat/module/kubernetes/util" + + dto "github.com/prometheus/client_model/go" +) + +func eventMapping(families []*dto.MetricFamily) ([]common.MapStr, error) { + eventsMap := map[string]common.MapStr{} + for _, family := range families { + for _, metric := range family.GetMetric() { + statefulset := util.GetLabel(metric, "statefulset") + if statefulset == "" { + continue + } + namespace := util.GetLabel(metric, "namespace") + statefulsetKey := namespace + "::" + statefulset + event, ok := eventsMap[statefulsetKey] + if !ok { + event = common.MapStr{} + eventsMap[statefulsetKey] = event + } + switch family.GetName() { + case "kube_statefulset_metadata_generation": + event.Put(mb.ModuleDataKey+".namespace", util.GetLabel(metric, "namespace")) + event.Put(mb.NamespaceKey, "statefulset") + event.Put("name", util.GetLabel(metric, "statefulset")) + event.Put("generation.desired", metric.GetGauge().GetValue()) + case "kube_statefulset_status_observed_generation": + event.Put("generation.observed", metric.GetGauge().GetValue()) + case "kube_statefulset_created": + event.Put("created", metric.GetGauge().GetValue()) + case "kube_statefulset_replicas": + event.Put("replicas.desired", metric.GetGauge().GetValue()) + case "kube_statefulset_status_replicas": + event.Put("replicas.observed", metric.GetGauge().GetValue()) + default: + // Ignore unknown metric + continue + } + } + } + + // initialize, populate events array from values in eventsMap + events := make([]common.MapStr, 0, len(eventsMap)) + for _, event := range eventsMap { + events = append(events, event) + } + return events, nil +} diff --git a/metricbeat/module/kubernetes/state_statefulset/state_statefulset.go b/metricbeat/module/kubernetes/state_statefulset/state_statefulset.go new file mode 100644 index 000000000000..bfeba9f15dea --- /dev/null +++ b/metricbeat/module/kubernetes/state_statefulset/state_statefulset.go @@ -0,0 +1,63 @@ +package state_statefulset + +import ( + "github.com/elastic/beats/libbeat/common" + "github.com/elastic/beats/metricbeat/helper" + "github.com/elastic/beats/metricbeat/mb" + "github.com/elastic/beats/metricbeat/mb/parse" +) + +const ( + defaultScheme = "http" + defaultPath = "/metrics" +) + +var ( + hostParser = parse.URLHostParserBuilder{ + DefaultScheme: defaultScheme, + DefaultPath: defaultPath, + }.Build() +) + +// init registers the MetricSet with the central registry. +// The New method will be called after the setup of the module and before starting to fetch data +func init() { + if err := mb.Registry.AddMetricSet("kubernetes", "state_statefulset", New, hostParser); err != nil { + panic(err) + } +} + +// MetricSet type defines all fields of the MetricSet +// As a minimum it must inherit the mb.BaseMetricSet fields, but can be extended with +// additional entries. These variables can be used to persist data or configuration between +// multiple fetch calls. +type MetricSet struct { + mb.BaseMetricSet + prometheus *helper.Prometheus +} + +// New create a new instance of the MetricSet +// Part of new is also setting up the configuration by processing additional +// configuration entries if needed. +func New(base mb.BaseMetricSet) (mb.MetricSet, error) { + prometheus, err := helper.NewPrometheusClient(base) + if err != nil { + return nil, err + } + return &MetricSet{ + BaseMetricSet: base, + prometheus: prometheus, + }, nil +} + +// Fetch methods implements the data gathering and data conversion to the right format +// It returns the event which is then forward to the output. In case of an error, a +// descriptive error must be returned. +func (m *MetricSet) Fetch() ([]common.MapStr, error) { + families, err := m.prometheus.GetFamilies() + if err != nil { + return nil, err + } + + return eventMapping(families) +} diff --git a/metricbeat/module/kubernetes/state_statefulset/state_statefulset_test.go b/metricbeat/module/kubernetes/state_statefulset/state_statefulset_test.go new file mode 100644 index 000000000000..2e6dd8a8ec48 --- /dev/null +++ b/metricbeat/module/kubernetes/state_statefulset/state_statefulset_test.go @@ -0,0 +1,111 @@ +// +build !integration + +package state_statefulset + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/elastic/beats/libbeat/common" + mbtest "github.com/elastic/beats/metricbeat/mb/testing" +) + +const testFile = "../_meta/test/kube-state-metrics" + +func TestEventMapping(t *testing.T) { + file, err := os.Open(testFile) + assert.NoError(t, err, "cannot open test file "+testFile) + + body, err := ioutil.ReadAll(file) + assert.NoError(t, err, "cannot read test file "+testFile) + + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Header().Set("Content-Type", "text/plain; charset=ISO-8859-1") + w.Write([]byte(body)) + })) + + server.Start() + defer server.Close() + + config := map[string]interface{}{ + "module": "kubernetes", + "metricsets": []string{"state_statefulset"}, + "hosts": []string{server.URL}, + } + + f := mbtest.NewEventsFetcher(t, config) + + events, err := f.Fetch() + assert.NoError(t, err) + + assert.Equal(t, 3, len(events), "Wrong number of returned events") + + testCases := testCases() + for _, event := range events { + name, err := event.GetValue("name") + if err == nil { + namespace, err := event.GetValue("_module.namespace") + if err == nil { + eventKey := namespace.(string) + "@" + name.(string) + oneTestCase, oneTestCaseFound := testCases[eventKey] + if oneTestCaseFound { + for k, v := range oneTestCase { + testValue(t, event, k, v) + } + delete(testCases, eventKey) + } + } + } + } + + if len(testCases) > 0 { + t.Errorf("Test reference events not found: %v", testCases) + } +} + +func testValue(t *testing.T, event common.MapStr, field string, expected interface{}) { + data, err := event.GetValue(field) + assert.NoError(t, err, "Could not read field "+field) + assert.EqualValues(t, expected, data, "Wrong value for field "+field) +} + +func testCases() map[string]map[string]interface{} { + return map[string]map[string]interface{}{ + "default@elasticsearch": { + "_module.namespace": "default", + "name": "elasticsearch", + + "created": 1511973651, + "replicas.observed": 1, + "replicas.desired": 4, + "generation.observed": 1, + "generation.desired": 3, + }, + "default@mysql": { + "_module.namespace": "default", + "name": "mysql", + + "created": 1511989697, + "replicas.observed": 2, + "replicas.desired": 5, + "generation.observed": 2, + "generation.desired": 4, + }, + "custom@mysql": { + "_module.namespace": "custom", + "name": "mysql", + + "created": 1511999697, + "replicas.observed": 3, + "replicas.desired": 6, + "generation.observed": 3, + "generation.desired": 5, + }, + } +} diff --git a/metricbeat/modules.d/kubernetes.yml.disabled b/metricbeat/modules.d/kubernetes.yml.disabled index cf00d10cd454..b15263cb7f4a 100644 --- a/metricbeat/modules.d/kubernetes.yml.disabled +++ b/metricbeat/modules.d/kubernetes.yml.disabled @@ -16,6 +16,7 @@ - state_node - state_deployment - state_replicaset + - state_statefulset - state_pod - state_container period: 10s