KubeSense Metrics
Metrics are stored in VictoriaMetrics and queried with PromQL (plus MetricsQL extensions — see below). Requires the KubeSense MCP server; see kubesense-mcp for connection and auth.
Tools
| Tool | Purpose | Notes |
|---|---|---|
get-available-metrics |
List metric names | search_keywords are AND-matched, case-insensitive substrings |
get-metric-labels |
List label names on one metric | name required |
analyze-metrics |
Execute PromQL | from_time, to_time, query_type, promql all required |
All three resolve RBAC against the infrastructure module — a permission error here
means the caller lacks infrastructure access, not that the metric is missing.
Discovery-First Rule
Never guess a metric name. Metric names are deployment-specific: they depend on which collectors a cluster ships (kube-state-metrics vs OTel, node-exporter vs hostmetrics). A query against a metric that does not exist returns an empty result that is indistinguishable from "the value is zero".
get-available-metrics → get-metric-labels → analyze-metrics
get-available-metrics defaults to the last 1 hour. A sparse metric that reported
90 minutes ago will not appear — widen from_time/to_time before concluding it does
not exist.
{ "search_keywords": ["container", "memory"], "limit": 40 }
Both keywords must appear in the name, so ["container","memory"] matches
container_memory_working_set_bytes but not container_cpu_usage_seconds_total.
get-metric-labels returns label names only, not values. To see values, query the
metric with analyze-metrics and read the series labels off the result.
Label Conventions (the #1 cause of empty results)
[!IMPORTANT] The cluster label is
clusterId— camelCase, no underscore. Notcluster, notkubesense_cluster, notcluster_id. Every kube-state-metrics / cAdvisor series in KubeSense carries it.
| Concept | Label on k8s/cAdvisor metrics | Label on OTel hostmetrics |
|---|---|---|
| cluster | clusterId |
kubesense_cluster |
| host / node | node |
host_name |
| environment | — | kubesense_env_type |
| namespace | namespace |
— |
| pod | pod |
— |
| container | container |
— |
| workload | deployment / daemonset / statefulset (per metric family) |
— |
The two families exist because a cluster ships telemetry either via
node-exporter/process-exporter or via the OpenTelemetry collector. Run
get-metric-labels to see which convention a given metric follows rather than assuming.
Cloud-provider metrics (AWS/GCP/Azure/Atlas/Confluent/Kong) are a third convention with none of these labels — see Cloud Resource Metrics below.
Process metrics: the exporter family uses groupname (formatted {pid}-{command});
the OTel family splits it into process_pid, process_command_line, and
process_executable_name.
Writing Queries
{
"from_time": "2026-07-30T10:00:00Z",
"to_time": "2026-07-30T11:00:00Z",
"query_type": "range",
"promql": "sum(rate(container_cpu_usage_seconds_total{container!='',clusterId='prod-us'}[5m])) by (namespace)"
}
query_type: "range"→ a time series (trends)."instant"→ one scalar per series (totals, current state, top-N).- Step is computed for you:
max(window/30, 15)seconds. You cannot set it. A 1-hour window yields a 120s step, so arate()range shorter than that will have gaps — keeprate()windows at or above the step ([5m]is a safe default).
Two container-metric rules
- Always filter
container!=''oncontainer_*metrics. cAdvisor emits an extra pod-level rollup series with an emptycontainerlabel; without the filter everysum()double-counts. - CPU is in cores.
rate(container_cpu_usage_seconds_total[5m])yields cores; multiply by 1000 for millicores, which is how KubeSense's own UI reports it.
kube_pod_container_resource_limits / _requests
These are a single metric for both CPU and memory, discriminated by a resource
label — not separate metrics. Select it:
kube_pod_container_resource_limits{resource="memory"}
kube_pod_container_resource_limits{resource="cpu"}
Memory-usage-vs-limit is therefore a join, not a subtraction of two metrics:
sum by (pod, namespace) (container_memory_working_set_bytes{container!=''})
/ sum by (pod, namespace) (kube_pod_container_resource_limits{resource="memory"}) * 100
kube_*_status_phase / _condition are label-encoded
kube_pod_status_phase is 1/0 per phase, with the phase in a label — so counting
running pods means selecting the label and de-duplicating series:
count(max by (pod, namespace, clusterId) (kube_pod_status_phase{phase="Running"}) > 0)
Same shape for nodes: kube_node_status_condition{condition="Ready",status="true"}.
The max by (...) de-dup matters — duplicate label sets otherwise inflate the count.
MetricsQL Extensions
The backend is VictoriaMetrics, so MetricsQL is available on top of PromQL. Two extensions KubeSense itself relies on:
| Extension | Use |
|---|---|
default 0 |
Substitute a value where the series is absent: sum(...) default 0 |
drop_empty_series(...) |
Discard series that are entirely empty after filtering |
[1h:60s] subqueries |
Rollup-over-rollup, e.g. max_over_time(rate(x[5m])[1h:60s]) |
These will not work against a stock Prometheus, but they are correct here.
Common Patterns
| Goal | Query |
|---|---|
| Per-second counter rate | rate(metric[5m]) |
| Aggregate across series | sum(...) by (namespace) |
| Histogram percentile | histogram_quantile(0.99, sum(rate(metric_bucket[5m])) by (le)) |
| Total over a window | increase(metric[1h]) |
| Top N | topk(10, ...) — pair with query_type: "instant" |
| Regex label match | metric{namespace=~"prod-.*"} |
| Ratio as a percentage | sum(a) / sum(b) * 100 |
For a histogram percentile, always sum(rate(...)) by (le) before
histogram_quantile — passing raw buckets grouped by other labels produces nonsense.
Cloud Resource Metrics
Metrics pulled from cloud providers' own monitoring APIs — AWS CloudWatch, Google Cloud Monitoring, Azure Monitor, MongoDB Atlas, Confluent Cloud, Kong — are in the same VictoriaMetrics instance but follow a different convention entirely.
[!IMPORTANT] Every cloud datapoint, for every provider and resource type, is stored under one series name:
kubesense_cloud_resource_metric. The provider's own metric name is themetric_namelabel, not part of the series name.get-available-metricstherefore returns exactly one name no matter how many cloud series exist, and searching it forCPUUtilizationfinds nothing.
kubesense_cloud_resource_metric{provider="aws",resource_type="Ec2Instance",metric_name="CPUUtilization"}
Fixed labels: provider (aws | gcp | azure | mongodbatlas | confluent |
kong), account_id, resource_id, resource_type, metric_name, unit — plus
collector dimensions such as region. There is no clusterId, namespace or pod,
so a selector copied from a Kubernetes query matches nothing.
Discovery is a PromQL step, not a tool call, because the values live in labels:
count by (resource_type) (kubesense_cloud_resource_metric{provider="aws"})
count by (metric_name, unit) (kubesense_cloud_resource_metric{resource_type="RdsInstance"})
Three rules that decide whether a cloud query is right or silently wrong:
- Never
rate()them. The provider's aggregation (CloudWatchSum/Average, the Cloud Monitoring aligner, the Azure Monitor aggregation) is already applied at collection time. Use the raw value, orsum_over_time/avg_over_time/max_over_timeto roll up buckets. - Always pin
resource_typealongsidemetric_name.CPUUtilizationalone mixes EC2, RDS, ECS, ElastiCache, DocumentDB, Neptune, OpenSearch and Redshift into one aggregate. - Collection is a 5-minute tick with a 15-minute forward-fill. Instant queries work because the newest sample is re-stamped; a series silent for more than 15 minutes is genuinely absent, not zero. Keep windows at or above 10m.
Full label contract, per-provider resource types and their metrics, the AWS types that
roll up onto a parent (there is no PerformanceInsights or NetworkLoadBalancer
resource type), and worked queries:
references/cloud-metric-catalog.md.
Choosing Metrics vs Traces vs Infra Tools
- Resource pressure, saturation, capacity (CPU, memory, disk, GPU) → metrics.
- Request rate, latency percentiles, error rate per service/endpoint → traces
(
analyze-traces). Metrics have no per-request detail. - "Is this pod restarting / why" or "what's running" → kubesense-infra tools; they return the same facts already joined and labelled, in one call.
Use metrics to quantify and confirm a hypothesis the cheaper tools surfaced.
Metric Catalog
For the metric families KubeSense collects — exact names for pods, containers, nodes, workloads, PVCs, network, GPU, JVM, and process metrics — read references/metric-catalog.md. For AWS, GCP, Azure, MongoDB Atlas, Confluent Cloud and Kong resource metrics, read references/cloud-metric-catalog.md.
Treat the catalog as what to expect, not a substitute for discovery: which families
are present depends on the cluster's collectors, and application metrics are entirely
deployment-specific. Always confirm with get-available-metrics.
Rules
get-available-metricsbefore writing PromQL. Never invent a metric name.get-metric-labelsbefore writing a label selector — and remember the cluster label isclusterId.- Filter
container!=''on everycontainer_*metric. rate()windows must be ≥ the auto-computed step (max(window/30, 15)s);[5m]is a safe default.kube_pod_container_resource_limits/_requestsneed aresource="cpu"|"memory"selector — they are one metric, not two.kube_*_status_phase/_conditionencode the state in a label; select it and de-dup withmax by (...)before counting.query_type: "instant"for totals/current state/top-N;"range"for trends.- An empty result means "no matching series", which is NOT the same as zero — verify the metric exists and the labels match before reporting a value of 0.
sum(rate(...)) by (le)beforehistogram_quantile.- Widen the discovery window for sparse metrics;
get-available-metricsonly looks back 1 hour by default. - Cloud-provider metrics are all one series —
kubesense_cloud_resource_metric— with the provider's metric name in themetric_namelabel. Discover them withcount by (metric_name) (...), neverget-available-metrics. - Never
rate()a cloud metric, and always pinresource_typenext tometric_name.