Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(service): add kube_service_ports metric #2607

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/metrics/service/service-metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
| kube_service_spec_type | Gauge | Type about service | | `service`=&lt;service-name&gt; <br> `namespace`=&lt;service-namespace&gt; <br> `uid`=&lt;service-uid&gt; <br> `type`=&lt;ClusterIP\|NodePort\|LoadBalancer\|ExternalName&gt; | STABLE |
| kube_service_spec_external_ip | Gauge | Service external ips. One series for each ip | | `service`=&lt;service-name&gt; <br> `namespace`=&lt;service-namespace&gt; <br> `uid`=&lt;service-uid&gt; <br> `external_ip`=&lt;external-ip&gt; | STABLE |
| kube_service_status_load_balancer_ingress | Gauge | Service load balancer ingress status | | `service`=&lt;service-name&gt; <br> `namespace`=&lt;service-namespace&gt; <br> `uid`=&lt;service-uid&gt; <br> `ip`=&lt;load-balancer-ingress-ip&gt; <br> `hostname`=&lt;load-balancer-ingress-hostname&gt; | STABLE |
| kube_service_ports | Gauge | Metric providing details about the ports exposed by services. | | `service`=&lt;service-name&gt; <br> `namespace`=&lt;service-namespace&gt; <br> `uid`=&lt;service-uid&gt; <br> `port_name`=&lt;service-port-name&gt; <br> `port_protocol`=&lt;service-port-protocol&gt; <br> `port_number`=&lt;service-port-number&gt; <br> `port_target`=&lt;service-port-target&gt; <br> `port_app_protocol`=&lt;service-port-appProtocol&gt; <br> `port_node_number`=&lt;service-node-port-number&gt; | EXPERIMENTAL |
44 changes: 44 additions & 0 deletions internal/store/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package store

import (
"context"
"strconv"

v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -184,6 +185,49 @@ func serviceMetricFamilies(allowAnnotationsList, allowLabelsList []string) []gen
}
}),
),
*generator.NewFamilyGeneratorWithStability(
"kube_service_ports",
"Metric providing details about the ports exposed by services.",
metric.Gauge,
basemetrics.ALPHA,
"",
wrapSvcFunc(func(e *v1.Service) *metric.Family {
ms := []*metric.Metric{}
labelKeys := []string{"port_protocol", "port_number", "port_name", "port_target", "port_node_number", "port_app_protocol"}
Copy link
Contributor

@CatherineF-dev CatherineF-dev Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has the cardinality issue.

Also, could you share what's the use case of this metric? Which alert will be added for this metric?

Copy link
Author

@nahuel11500 nahuel11500 Mar 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello,

Thanks for your answer.

For our use case, we want to implement a dashboard that can show the different ports available on a service. And then connect more easily on them with Telepresence.

Regarding the cardinality issue, while we understand the concern, we believe it should be manageable.

for _, port := range e.Spec.Ports {
appProtocol := ""
if port.AppProtocol != nil {
appProtocol = *port.AppProtocol
}
var labelValues []string
portNumber := strconv.FormatInt(int64(port.Port), 10)
targetPort := port.TargetPort.String()
nodePort := ""

if port.NodePort != 0 {
nodePort = strconv.FormatInt(int64(port.NodePort), 10)
}

labelValues = []string{
string(port.Protocol),
portNumber,
port.Name,
targetPort,
nodePort,
appProtocol,
}

ms = append(ms, &metric.Metric{
LabelValues: labelValues,
LabelKeys: labelKeys,
Value: 1,
})
}
return &metric.Family{
Metrics: ms,
}
}),
),
}
}

Expand Down
35 changes: 35 additions & 0 deletions internal/store/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"

generator "k8s.io/kube-state-metrics/v2/pkg/metric_generator"
)
Expand All @@ -44,6 +45,8 @@ func TestServiceStore(t *testing.T) {
# TYPE kube_service_spec_external_ip gauge
# HELP kube_service_status_load_balancer_ingress [STABLE] Service load balancer ingress status
# TYPE kube_service_status_load_balancer_ingress gauge
# HELP kube_service_ports Metric providing details about the ports exposed by services.
# TYPE kube_service_ports gauge
`
cases := []generateMetricsTestCase{
{
Expand All @@ -68,11 +71,13 @@ func TestServiceStore(t *testing.T) {
# HELP kube_service_info [STABLE] Information about service.
# HELP kube_service_labels [STABLE] Kubernetes labels converted to Prometheus labels.
# HELP kube_service_spec_type [STABLE] Type about service.
# HELP kube_service_ports Metric providing details about the ports exposed by services.
# TYPE kube_service_annotations gauge
# TYPE kube_service_created gauge
# TYPE kube_service_info gauge
# TYPE kube_service_labels gauge
# TYPE kube_service_spec_type gauge
# TYPE kube_service_ports gauge
kube_service_created{namespace="default",service="test-service1",uid="uid1"} 1.5e+09
kube_service_info{cluster_ip="1.2.3.4",external_name="",external_traffic_policy="",load_balancer_ip="",namespace="default",service="test-service1",uid="uid1"} 1
kube_service_spec_type{namespace="default",service="test-service1",type="ClusterIP",uid="uid1"} 1
Expand All @@ -83,6 +88,7 @@ func TestServiceStore(t *testing.T) {
"kube_service_info",
"kube_service_labels",
"kube_service_spec_type",
"kube_service_ports",
},
},
{
Expand Down Expand Up @@ -259,6 +265,35 @@ func TestServiceStore(t *testing.T) {
kube_service_spec_type{namespace="default",service="test-service8",uid="uid8",type="LoadBalancer"} 1
`,
},

{
Obj: &v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service9",
CreationTimestamp: metav1.Time{Time: time.Unix(1500000000, 0)},
Namespace: "default",
UID: "uid9",
Labels: map[string]string{
"app": "example9",
},
},
Spec: v1.ServiceSpec{
ClusterIP: "1.2.3.14",
LoadBalancerIP: "1.2.3.15",
Type: v1.ServiceTypeLoadBalancer,
ExternalTrafficPolicy: "Local",
Ports: []v1.ServicePort{
{Port: 80, Protocol: v1.ProtocolTCP, TargetPort: intstr.FromInt(8080), Name: "http", NodePort: 65000, AppProtocol: func(s string) *string { return &s }("grpc")},
},
},
},
Want: metadata + `
kube_service_created{namespace="default",service="test-service9",uid="uid9"} 1.5e+09
kube_service_info{cluster_ip="1.2.3.14",external_name="",external_traffic_policy="Local",load_balancer_ip="1.2.3.15",namespace="default",service="test-service9",uid="uid9"} 1
kube_service_spec_type{namespace="default",service="test-service9",uid="uid9",type="LoadBalancer"} 1
kube_service_ports{namespace="default",port_app_protocol="grpc",port_name="http",port_node_number="65000",port_number="80",port_protocol="TCP",port_target="8080",service="test-service9",uid="uid9"} 1
`,
},
}
for i, c := range cases {
c.Func = generator.ComposeMetricGenFuncs(serviceMetricFamilies(nil, nil))
Expand Down
Loading