diff --git a/docs/README.md b/docs/README.md index a2903a32d3..132cf4cabf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -54,6 +54,7 @@ Per group of metrics there is one file for each metrics. See each file for speci - [Horizontal Pod Autoscaler Metrics](horizontalpodautoscaler-metrics.md) - [Ingress Metrics](ingress-metrics.md) - [Job Metrics](job-metrics.md) +- [Lease Metrics](lease-metrics.md) - [LimitRange Metrics](limitrange-metrics.md) - [MutatingWebhookConfiguration Metrics](mutatingwebhookconfiguration.md) - [Namespace Metrics](namespace-metrics.md) diff --git a/docs/cli-arguments.md b/docs/cli-arguments.md index eb8f95d91f..2a22807b60 100644 --- a/docs/cli-arguments.md +++ b/docs/cli-arguments.md @@ -28,7 +28,7 @@ Usage of ./kube-state-metrics: --add_dir_header If true, adds the file directory to the header --alsologtostderr log to standard error as well as files --apiserver string The URL of the apiserver to use as a master - --collectors string Comma-separated list of collectors to be enabled. Defaults to "certificatesigningrequests,configmaps,cronjobs,daemonsets,deployments,endpoints,horizontalpodautoscalers,ingresses,jobs,limitranges,mutatingwebhookconfigurations,namespaces,networkpolicies,nodes,persistentvolumeclaims,persistentvolumes,poddisruptionbudgets,pods,replicasets,replicationcontrollers,resourcequotas,secrets,services,statefulsets,storageclasses,validatingwebhookconfigurations,volumeattachments" + --collectors string Comma-separated list of collectors to be enabled. Defaults to "certificatesigningrequests,configmaps,cronjobs,daemonsets,deployments,endpoints,horizontalpodautoscalers,ingresses,jobs,leases,limitranges,mutatingwebhookconfigurations,namespaces,networkpolicies,nodes,persistentvolumeclaims,persistentvolumes,poddisruptionbudgets,pods,replicasets,replicationcontrollers,resourcequotas,secrets,services,statefulsets,storageclasses,validatingwebhookconfigurations,volumeattachments" --disable-node-non-generic-resource-metrics Disable node non generic resource request and limit metrics --disable-pod-non-generic-resource-metrics Disable pod non generic resource request and limit metrics --enable-gzip-encoding Gzip responses when requested by clients via 'Accept-Encoding: gzip' header. diff --git a/docs/lease-metrics.md b/docs/lease-metrics.md new file mode 100644 index 0000000000..8d995116a3 --- /dev/null +++ b/docs/lease-metrics.md @@ -0,0 +1,6 @@ +# Lease Metrics + +| Metric name| Metric type | Labels/tags | Status | +| ---------- | ----------- | ----------- | ----------- | +| kube_lease_owner | Gauge | `lease`=<lease-name>
`owner_kind`=<onwer kind>
`owner_name`=<owner name> | EXPERIMENTAL | +| kube_lease_renew_time | Gauge | `lease`=<lease-name> | EXPERIMENTAL | diff --git a/examples/autosharding/cluster-role.yaml b/examples/autosharding/cluster-role.yaml index cfbd243694..4da4a33e74 100644 --- a/examples/autosharding/cluster-role.yaml +++ b/examples/autosharding/cluster-role.yaml @@ -108,3 +108,10 @@ rules: verbs: - list - watch +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - list + - watch diff --git a/examples/standard/cluster-role.yaml b/examples/standard/cluster-role.yaml index cfbd243694..4da4a33e74 100644 --- a/examples/standard/cluster-role.yaml +++ b/examples/standard/cluster-role.yaml @@ -108,3 +108,10 @@ rules: verbs: - list - watch +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - list + - watch diff --git a/internal/store/builder.go b/internal/store/builder.go index a98cfceb3d..49b76e6a81 100644 --- a/internal/store/builder.go +++ b/internal/store/builder.go @@ -30,6 +30,7 @@ import ( batchv1 "k8s.io/api/batch/v1" batchv1beta1 "k8s.io/api/batch/v1beta1" certv1beta1 "k8s.io/api/certificates/v1beta1" + coordinationv1 "k8s.io/api/coordination/v1" v1 "k8s.io/api/core/v1" extensions "k8s.io/api/extensions/v1beta1" policy "k8s.io/api/policy/v1beta1" @@ -168,6 +169,7 @@ var availableStores = map[string]func(f *Builder) cache.Store{ "horizontalpodautoscalers": func(b *Builder) cache.Store { return b.buildHPAStore() }, "ingresses": func(b *Builder) cache.Store { return b.buildIngressStore() }, "jobs": func(b *Builder) cache.Store { return b.buildJobStore() }, + "leases": func(b *Builder) cache.Store { return b.buildLeases() }, "limitranges": func(b *Builder) cache.Store { return b.buildLimitRangeStore() }, "mutatingwebhookconfigurations": func(b *Builder) cache.Store { return b.buildMutatingWebhookConfigurationStore() }, "namespaces": func(b *Builder) cache.Store { return b.buildNamespaceStore() }, @@ -314,6 +316,10 @@ func (b *Builder) buildVPAStore() cache.Store { return b.buildStore(vpaMetricFamilies, &vpaautoscaling.VerticalPodAutoscaler{}, createVPAListWatchFunc(b.vpaClient)) } +func (b *Builder) buildLeases() cache.Store { + return b.buildStore(leaseMetricFamilies, &coordinationv1.Lease{}, createLeaseListWatch) +} + func (b *Builder) buildStore( metricFamilies []generator.FamilyGenerator, expectedType interface{}, diff --git a/internal/store/lease.go b/internal/store/lease.go new file mode 100644 index 0000000000..f473d03667 --- /dev/null +++ b/internal/store/lease.go @@ -0,0 +1,113 @@ +/* +Copyright 2020 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package store + +import ( + coordinationv1 "k8s.io/api/coordination/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" + + "k8s.io/kube-state-metrics/pkg/metric" + generator "k8s.io/kube-state-metrics/pkg/metric_generator" +) + +var ( + descLeaseLabelsDefaultLabels = []string{"lease"} + + leaseMetricFamilies = []generator.FamilyGenerator{ + { + Name: "kube_lease_owner", + Type: metric.Gauge, + Help: "Information about the Lease's owner.", + GenerateFunc: wrapLeaseFunc(func(l *coordinationv1.Lease) *metric.Family { + labelKeys := []string{"owner_kind", "owner_name"} + + owners := l.GetOwnerReferences() + if len(owners) == 0 { + return &metric.Family{ + Metrics: []*metric.Metric{ + { + LabelKeys: labelKeys, + LabelValues: []string{"", ""}, + Value: 1, + }, + }, + } + } + ms := make([]*metric.Metric, len(owners)) + + for i, owner := range owners { + ms[i] = &metric.Metric{ + LabelKeys: labelKeys, + LabelValues: []string{owner.Kind, owner.Name}, + Value: 1, + } + } + + return &metric.Family{ + Metrics: ms, + } + }), + }, + { + Name: "kube_lease_renew_time", + Type: metric.Gauge, + Help: "Kube lease renew time.", + GenerateFunc: wrapLeaseFunc(func(l *coordinationv1.Lease) *metric.Family { + ms := []*metric.Metric{} + + if !l.Spec.RenewTime.IsZero() { + ms = append(ms, &metric.Metric{ + Value: float64(l.Spec.RenewTime.Unix()), + }) + } + return &metric.Family{ + Metrics: ms, + } + }), + }, + } +) + +func wrapLeaseFunc(f func(*coordinationv1.Lease) *metric.Family) func(interface{}) *metric.Family { + return func(obj interface{}) *metric.Family { + lease := obj.(*coordinationv1.Lease) + + metricFamily := f(lease) + + for _, m := range metricFamily.Metrics { + m.LabelKeys = append(descLeaseLabelsDefaultLabels, m.LabelKeys...) + m.LabelValues = append([]string{lease.Name}, m.LabelValues...) + } + + return metricFamily + } +} + +func createLeaseListWatch(kubeClient clientset.Interface, _ string) cache.ListerWatcher { + return &cache.ListWatch{ + ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { + return kubeClient.CoordinationV1().Leases("kube-node-lease").List(opts) + }, + WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { + return kubeClient.CoordinationV1().Leases("kube-node-lease").Watch(opts) + }, + } +} diff --git a/internal/store/lease_test.go b/internal/store/lease_test.go new file mode 100644 index 0000000000..a0d9618b4e --- /dev/null +++ b/internal/store/lease_test.go @@ -0,0 +1,74 @@ +/* +Copyright 2020 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package store + +import ( + "testing" + "time" + + coordinationv1 "k8s.io/api/coordination/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + generator "k8s.io/kube-state-metrics/pkg/metric_generator" +) + +func TestLeaseStore(t *testing.T) { + const metadata = ` + # HELP kube_lease_owner Information about the Lease's owner. + # TYPE kube_lease_owner gauge + # HELP kube_lease_renew_time Kube lease renew time. + # TYPE kube_lease_renew_time gauge + ` + + var ( + cases = []generateMetricsTestCase{ + { + Obj: &coordinationv1.Lease{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 2, + Name: "kube-master", + CreationTimestamp: metav1.Time{Time: time.Unix(1500000000, 0)}, + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "Node", + Name: "kube-master", + }, + }, + }, + Spec: coordinationv1.LeaseSpec{ + RenewTime: &metav1.MicroTime{Time: time.Unix(1500000000, 0)}, + }, + }, + Want: metadata + ` + kube_lease_owner{lease="kube-master",owner_kind="Node",owner_name="kube-master"} 1 + kube_lease_renew_time{lease="kube-master"} 1.5e+09 + `, + MetricNames: []string{ + "kube_lease_owner", + "kube_lease_renew_time", + }, + }, + } + ) + for i, c := range cases { + c.Func = generator.ComposeMetricGenFuncs(leaseMetricFamilies) + c.Headers = generator.ExtractMetricFamilyHeaders(leaseMetricFamilies) + if err := c.run(); err != nil { + t.Errorf("unexpected collecting result in %dth run:\n%v", i, err) + } + } +} diff --git a/jsonnet/kube-state-metrics/kube-state-metrics.libsonnet b/jsonnet/kube-state-metrics/kube-state-metrics.libsonnet index 1169a7b903..7b2b93748e 100644 --- a/jsonnet/kube-state-metrics/kube-state-metrics.libsonnet +++ b/jsonnet/kube-state-metrics/kube-state-metrics.libsonnet @@ -137,6 +137,13 @@ local k = import 'ksonnet/ksonnet.beta.4/k.libsonnet'; 'networkpolicies', ]) + rulesType.withVerbs(['list', 'watch']), + + rulesType.new() + + rulesType.withApiGroups(['coordination.k8s.io']) + + rulesType.withResources([ + 'leases', + ]) + + rulesType.withVerbs(['list', 'watch']), ]; clusterRole.new() + diff --git a/pkg/options/collector.go b/pkg/options/collector.go index f3afaf76c1..97232214d0 100644 --- a/pkg/options/collector.go +++ b/pkg/options/collector.go @@ -35,6 +35,7 @@ var ( "horizontalpodautoscalers": struct{}{}, "ingresses": struct{}{}, "jobs": struct{}{}, + "leases": struct{}{}, "limitranges": struct{}{}, "mutatingwebhookconfigurations": struct{}{}, "namespaces": struct{}{}, diff --git a/tests/manifests/lease.yaml b/tests/manifests/lease.yaml new file mode 100644 index 0000000000..197df2a30e --- /dev/null +++ b/tests/manifests/lease.yaml @@ -0,0 +1,14 @@ +apiVersion: coordination.k8s.io/v1 +kind: Lease +metadata: + name: kube-master + namespace: kube-node-lease + ownerReferences: + - apiVersion: v1 + kind: Node + name: kube-master + uid: 71c6ca9d-7a35-4380-b2ae-aca4d47800df +spec: + holderIdentity: kube-master + leaseDurationSeconds: 40 + renewTime: "2020-01-26T09:52:23.548762Z"