kube-state-graph

module
v0.1.20 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: Apache-2.0

README

kube-state-graph

Traditional Chinese: README.zh-tw.md.

A Go REST API server that returns a unified pod / node / PVC graph for one or more Kubernetes clusters, including pod-UID-resolved RPC edges that may cross cluster boundaries.

cluster A: kube-state-metrics ──┐
           service-graph source ┤
                                 │  (vmagent / Prometheus
cluster B: kube-state-metrics ──┤   with external_labels:
           service-graph source ┤   { cluster: "<name>" })
                                 │
       ...                       ├──► centralised VictoriaMetrics ◄── kube-state-graph
                                 │                                     (Prometheus HTTP API)
cluster N: kube-state-metrics ──┤
           service-graph source ─┘

What it does

  • Reads kube_* topology, Harvest / kubelet storage series, and traces_service_graph_* runtime metrics from a single centralised VictoriaMetrics, on demand for a caller-specified [start, end] time range. Every series the builder queries is listed in docs/upstream-metrics.md.
  • Joins them into a multi-cluster graph keyed by cluster-scoped pod UIDs and node names.
  • Returns the graph as Cytoscape.js JSON (/v1/graph).
  • Exposes a static edge-type catalogue (/v1/edge-types). The set of clusters with data is the clusters field of any /v1/graph response.
  • Builds the graph on every request — v1 ships no in-process result cache, no singleflight, and no HTTP cache validators (ETag / If-None-Match / 304). A horizontally scalable cache mechanism for distributed deployment is anticipated as a future change. Caller-supplied start / end accept RFC 3339 or Unix seconds; the server enforces only end > start, then passes the window through to upstream PromQL verbatim — no server-side bucketing, alignment, max-window cap, or future-time guard. Bounded query cost is delegated to VictoriaMetrics search limits (-search.maxQueryDuration, -search.maxPointsPerTimeseries, -search.maxSamplesPerQuery). The serialiser produces a deterministic body (apiVersion, clusters, elements only — no echoed time fields). Pod, node, and service IPs appear on the top-level ipaddress attribute, not in labels. Pods additionally carry typed data attributes — owner ({kind, name}), application (the ArgoCD Application), and containers ([{name, image}]) — all omitempty and never inside labels.
  • Narrows the build at the source: cluster, namespace, az and env are rendered into the upstream PromQL queries as label matchers, so VictoriaMetrics does the filtering before a sample crosses the wire. The service-graph series are deliberately read in full (see Request filters).

Quick start

make build
./bin/kube-state-graph \
  --prom-url=http://victoria-metrics.example:8428 \
  --listen-addr=:8080

Then:

curl 'http://localhost:8080/v1/graph?start=$(date -u -d "-5 min" +%s)&end=$(date -u +%s)' | jq '.elements'

# One namespace's storage topology, including workload with no traffic.
curl 'http://localhost:8080/v1/graph?start=…&end=…&namespace=payments&prune=false' | jq '.elements'

# One zone / environment.
curl 'http://localhost:8080/v1/graph?start=…&end=…&az=eu-west-1a&env=prod' | jq '.clusters'

When the server is started with API keys configured (--api-keys-file or --api-keys), every /v1/* request must carry an X-API-Key: <key> header:

curl -H 'X-API-Key: my-secret-key' 'http://localhost:8080/v1/edge-types'

Health probes (/livez, /readyz), /metrics, and the docs routes (/openapi.*, /docs) are exempt and require no key. With no keys configured the middleware is a no-op and every route is open.

Request filters

GET /v1/graph takes start, end (both required) plus six optional parameters. Values within one parameter are OR-combined, different parameters are AND-combined.

Parameter Applied Notes
cluster upstream and projection Repeatable. unknown addresses series carrying no cluster label.
namespace upstream and projection Repeatable. Narrows the pod-, claim-, Service- and EndpointSlice-scoped series; nodes and NetApp aggregates follow by reference.
az upstream Repeatable. Matched against --az-label (default az) on every topology query.
env upstream Repeatable. Matched against --env-label (default env).
edge_type projection Repeatable; validated against /v1/edge-types.
prune projection true (default) keeps only workload on a connectivity edge. false returns the inventory: every loaded pod with its node / PVC / NetApp chain, plus unreferenced infrastructure when no cluster or namespace filter narrows it.

Which matcher reaches which series is a hardcoded contract:

Series az env cluster namespace
pod / claim / Service / EndpointSlice KSM series, kubelet volume stats
kube_node_* — (no such label)
NetApp Harvest (volume_labels, qos_*, aggr_*, node_new_status) — (its cluster is the ONTAP cluster)
traces_service_graph_*, up

The service-graph family is read in full for every request: its cluster label is the frequently-missing trace-source cluster and its namespace labels describe only the caller's own view, so narrowing there would drop edges the loaded topology still needs. Instead, a filtered build applies two rules:

  • an endpoint whose pod is not loaded resolves as if its UID were empty — a "://" label can still reach a loaded Service, anything else becomes external/<label> (with empty labels), and no synthesised pod is ever created;
  • a series is kept only if at least one endpoint reaches loaded topology, so the out-of-scope estate never renders as a web of external nodes.

The visible consequence: under ?cluster= or ?namespace=, a peer outside the filter appears as an external node rather than a real pod — the request's inbound and outbound dependencies stay visible without loading the rest of the estate.

Operator precondition. The kube-state-metrics and kubelet families must carry the configured az / env labels. A family that does not simply matches nothing under those filters, and because the default projection keeps only connectivity-connected workload, a missing label can turn a filtered request into an empty graph rather than a partial one. The NetApp Harvest family is exempt: it carries no request matcher — ?az= selects which harvest backend of the routing table is asked, ?env= does not reach it — so Harvest series need no az / env label.

Upstream metrics consumed

The complete operator catalog — all 41 series, PromQL wrappers, fixed selectors, query-error vs empty-vector semantics, and the per-request fan-out — is docs/upstream-metrics.md.

Summary: one /v1/graph request fans out 37 topology queries in parallel (20 kube-state-metrics abort-on-error + 2 accumulating-cardinality annotation families log-and-continue + 13 Harvest + 2 kubelet log-and-continue), then 3 service-graph queries (skipped when a filtered build loaded neither pods nor services), plus up{} only for an unfiltered empty topology. There is no metric-name prefix; every series is queried at its bare name. v1 has no result cache.

Every Kubernetes-shaped series is expected to carry a cluster external label (injected by vmagent / Prometheus external_labels per source cluster). Harvest's cluster is the ONTAP cluster and is never used as ?cluster=.

The "Required?" column below is about an empty vector (the series is absent from the store, or matched nothing in the window). A query error (timeout / 5xx) on any of the 20 abort-on-error kube-state-metrics legs or on traces_service_graph_request_total fails the build; kube_replicaset_annotations and kube_job_annotations (cardinality accumulates with history, not live object count), Harvest, kubelet, and the two RED series log-and-continue. Details in the catalog.

Topology metrics — produced by kube-state-metrics
Metric Used for Labels read Required?
kube_pod_info Pod nodes (node label drives the pod-to-node edge; pods nest under the cluster > namespace > application > controller > pod workload hierarchy) cluster, namespace, pod, uid, node, pod_ip (→ data.ipaddress; host_ip not exported) Yes
kube_node_info K8sNode nodes cluster, node Yes
kube_node_status_addresses{type=~"ExternalIP|InternalIP"} Node data.ipaddress — ExternalIP preferred, InternalIP fallback when the node has no ExternalIP cluster, node, type, address Optional (absent ⇒ no ipaddress)
kube_node_status_condition{condition="Ready"} Node Ready status data.ready_status ∈ {Ready, NotReady, Unknown} from the active (status value 1) row; omitted when no Ready data — distinct from Unknown (kubelet lost contact) cluster, node, condition, status Optional (absent ⇒ no data.ready_status); a KSM default
kube_node_labels Node label propagation (kubernetes.io/* etc.) cluster, node, label_* Optional
kube_pod_spec_volumes_persistentvolumeclaims_info PVC nodes; pod-mounts-pvc edges cluster, namespace, pod, persistentvolumeclaim, volume Optional (no PVCs ⇒ no PVC nodes/edges)
kube_persistentvolumeclaim_info PVC data.storageclass (the policy name, never a node) + labels.volumename (bound PV name; roots the Harvest join) cluster, namespace, persistentvolumeclaim, storageclass, volumename Optional (absent ⇒ no data.storageclass / volumename; no Harvest join)
kube_pod_owner Pod controller-owner attribute data.owner = {kind, name} (ReplicaSet skipped to its Deployment; omitted when no controller owner). The resolved owner is also the join key for the pod's ArgoCD Application, and both drive the application / controller compound groups in the workload hierarchy cluster, namespace, pod, owner_kind, owner_name, owner_is_controller Optional (absent ⇒ no data.owner, and no data.application — the Application is keyed on the controller)
kube_replicaset_owner Resolves a ReplicaSet pod-owner up to its owning Deployment cluster, namespace, replicaset, owner_kind, owner_name Optional (absent ⇒ ReplicaSet kept as owner)
kube_job_owner{owner_kind="CronJob",owner_is_controller="true"} Resolves a Job up to its owning CronJob, for pod ArgoCD Application resolution only — the Kubernetes CronJob controller copies only spec.jobTemplate.metadata annotations onto the Jobs it creates, so ArgoCD's tracking-id never reaches a Job. Never alters data.owner cluster, namespace, job_name, owner_kind, owner_name, owner_is_controller Optional (absent ⇒ CronJob-managed pods carry no data.application); a KSM default
kube_{deployment,statefulset,daemonset,replicaset,job,cronjob}_annotations{annotation_argocd_argoproj_io_tracking_id!=""} Pod ArgoCD Application data.application (segment before the first : of the tracking-id), joined on (cluster, namespace, kind, name) against the pod's resolved controller owner — ArgoCD stamps the annotation on the workload object it applies, never on the pods a controller spawns. Nests the pod under the application compound group cluster, namespace, the family's identity label (deployment / statefulset / daemonset / replicaset / job_name / cronjob), annotation_argocd_argoproj_io_tracking_id Optional, per family (absent ⇒ no data.application for pods of that controller kind). Each requires --metric-annotations-allowlist=<plural-resource>=[argocd.argoproj.io/tracking-id] (NOT a KSM default). On a query error replicaset / job log-and-continue (cardinality accumulates with history); the other four fail the build
kube_pod_container_info Pod container list data.containers = [{name, image}], sorted by (name, image); on a mid-window image change the latest-seen image wins per container cluster, namespace, pod, container, image Optional (absent ⇒ no data.containers); a KSM default
kube_service_info Service nodes for :// connection-string resolution (D29); cluster_ip (headless None ⇒ no data.ipaddress) cluster, namespace, service, cluster_ip Optional (absent ⇒ :// endpoints fall back to external)
kube_service_annotations Service ArgoCD Application data.application (segment before the first : of the tracking-id), which nests the service under the application compound group cluster, namespace, service, annotation_argocd_argoproj_io_tracking_id Optional (absent ⇒ no data.application). Requires --metric-annotations-allowlist=services=[argocd.argoproj.io/tracking-id] (NOT a KSM default)
kube_persistentvolumeclaim_annotations PVC ArgoCD Application data.application (same parse as the service), which nests the PVC under the application compound group. An app-less PVC additionally inherits the lexically-smallest Application among the pods that mount it cluster, namespace, persistentvolumeclaim, annotation_argocd_argoproj_io_tracking_id Optional (absent ⇒ no own annotation; inheritance may still fill data.application). Requires --metric-annotations-allowlist=persistentvolumeclaims=[argocd.argoproj.io/tracking-id] (NOT a KSM default)
kube_endpointslice_endpoints Service → backing-pod fan-out (service-selects-pod edges) cluster, namespace, endpointslice, targetref_kind, targetref_namespace, targetref_name Optional
kube_endpointslice_labels Joins an EndpointSlice to its owning Service cluster, namespace, endpointslice, label_kubernetes_io_service_name Optional — requires --metric-labels-allowlist=endpointslices=[kubernetes.io/service-name] (NOT a KSM default); absent ⇒ no service-selects-pod resolution

These 22 series come from eleven kube-state-metrics collectors (pods, nodes, services, persistentvolumeclaims, replicasets, endpointslices, deployments, statefulsets, daemonsets, jobs, cronjobs), needing list + watch on those resource kinds and nothing else. The last five exist solely for pod ArgoCD Applications: skip them and the graph is unchanged except that pods carry no data.application. A minimal Helm values file, the exact ClusterRole it generates, and the cluster / az / env external-label requirements are in docs/kube-state-metrics-preconditions.md.

Harvest + kubelet storage metrics
Metric Used for Labels read Required?
volume_labels Hop A — the whole storage topology. PVC→aggregate join (volume_name = PV name), the netapp-aggr / netapp-node entities, and the PVC svm label. An info series: its value is ignored, only its labels are read cluster (ONTAP cluster), node, aggr, svm, volume_name Optional (absent ⇒ no NetApp nodes / edges / svm)
qos_read_ops / qos_write_ops / qos_read_latency / qos_write_latency / qos_read_data / qos_write_data Hop B — I/O on pvc-to-netapp-aggr (read_ops, write_ops, read_latency_us, write_latency_us, read_bytes_per_sec, write_bytes_per_sec). Read verbatim — Harvest already resolves ONTAP counters (ops/s, average µs, bytes/s); never wrapped in rate(). Queried at {lun=""} so a LUN workload, which carries its FlexVol's volume_name, is never summed on top cluster, svm, policy_group, lun, volume_name Optional (absent ⇒ edge with no metrics)
qos_policy_fixed_max_throughput_iops / qos_policy_fixed_max_throughput_mbps Hop C — the declared ceiling max_iops / max_bytes_per_sec on the same edge, joined on (cluster, svm, policy_group). The mbps figure is the one converted value (× 1048576 → bytes/s) so it shares the unit of read_bytes_per_sec cluster, svm, name (or policy_group) Optional (absent ⇒ no ceiling fields; never 0)
aggr_new_status Aggregate data.health (online if sample is 1, else degraded; omitted if no series) cluster, node, aggr Optional
aggr_space_used / aggr_space_total Aggregate data.usage {used_bytes, capacity_bytes} cluster, node, aggr Optional
node_new_status Controller data.health (same mapping as aggregate) cluster, node Optional
kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes PVC data.usage {used_bytes, capacity_bytes} cluster, namespace, persistentvolumeclaim Optional

volume_name is not a stock Harvest label — it is produced by the deployment's own relabel rule, which must stamp both the volume-object and the QoS workload series. See docs/netapp-harvest-preconditions.md.

Each is wrapped in last_over_time(<metric>[<window>]) @ <end> so the result reflects the most recent value within the requested [start, end] window — except kube_pod_container_info, which uses tlast_over_time(...) so each per-image series carries its last-sample timestamp, letting the reader pick the latest image per container (a recency pick that is accurate for near-now windows; see design.md D-A4 for the far-past-window caveat).

Service-graph metric — produced by Tempo or compatible generator
Metric Used for Labels read Required?
traces_service_graph_request_total pod-calls-pod (intra/cross-cluster), pod-calls-service (may cross-cluster — the route-engine path anchors on the selected ingress cluster), service-selects-pod (may cross-cluster) edges; denominator for data.metrics.rate. A query error fails the build; an empty vector does not cluster, client, server, client_k8s_pod_uid, server_k8s_pod_uid, plus peer-address labels used only for server="unknown" enrichment (client_server_address, then client_network_peer_address, then client_net_peer_name) Optional as data (no series ⇒ no call edges); query error is fatal
traces_service_graph_request_failed_total data.metrics.error_rate on measured edges Same identity labels as _total (joined by exact series identity minus __name__) Optional — absence / query error omits error_rate (never reports 0)
traces_service_graph_request_server_seconds_bucket data.metrics.p90_server_ms on measured edges (server-observed classic histogram) Same identity labels as _total, plus le — read raw, no upstream aggregation, joined by identity minus le Optional — absence / non-classic buckets omit p90_server_ms
edge_relation (a dimension, not a metric) Value link marks a connector-materialised span-link edge: the edge is still emitted, but it measures a queue/DB hop rather than a request, so it contributes nothing to data.metrics Read on all three series; excluded from the two RED selectors via edge_relation!="link" Optional — a producer that does not set it is unaffected (a negative matcher retains series where the label is absent)

Wrapped in rate(traces_service_graph_request_total[<window>]) @ <end>. Each series carries a single cluster external label representing the trace source (typically the cluster running Tempo's metrics-generator); this is the client-side cluster of the call. The server-side cluster is recovered at build time by joining server_k8s_pod_uid against the global topology pod-UID index — Kubernetes pod UIDs are unique across clusters in practice, so the lookup is unambiguous. Edges are only emitted when both endpoints resolve. When an endpoint's pod-UID label is empty, the human-readable client/server label is resolved by built-in connection-string detection (no knob): a label containing the literal :// is parsed as a URL — an in-cluster <service>.<namespace>.svc name becomes a single type="service" node in the caller's own cluster (so this connection-string path is always intra-cluster; the route-engine path below may anchor on a family sibling, which is why the edge type is registered may_cross_cluster: true), provided that cluster holds the same-named Service. That service node then fans out on-demand service-selects-pod edges to its backing pods across every same-family cluster holding the same-named Service — so service-selects-pod may cross clusters, modelling multi-cluster service-mesh endpoint aggregation (clusters are one family when their names match after collapsing digit runs, e.g. prod-1prod-2). A headless <pod>.<service>.<namespace>.svc name resolves to the same service node (the leading pod-hostname is dropped) — a :// endpoint is never a specific pod. An unresolvable URL, or one whose caller cluster does not hold the Service, becomes an external node. A non-URL label (no ://) also becomes an external node via the missing pod-UID human-label fallback.

RED metrics on edges (data.metrics)

An edge receives a typed data.metrics object iff it is trace-derived (produced by at least one traces_service_graph_request_total series) and both endpoints resolved to a type="pod" node (real or synthesised) or a type="service" node. How the endpoint was identified does not matter — a pod UID, a :// connection string, a server="unknown" peer address matched to a Service ClusterIP or straight to a Pod IP, and an Istio route-engine resolution all qualify — so both pod-calls-pod and pod-calls-service edges can be measured.

No metrics key at all on:

  • any edge with an external endpoint (the external node collapses every destination sharing one label string onto one identity);
  • synthesised edges: service-selects-pod fan-out, the ingress chain's gateway-pod → backend-service hop, and topology edges (pod-to-node, pod-mounts-pvc);
  • the route-hit ingress chain's caller → ingress-service entry hop. One series produces both that hop and the retained caller → backend edge; they are two projections of the same call, so only the backend edge — the one naming the actual destination — is measured, and summing rate across the chain never double-counts;
  • an edge whose contributing series all carry edge_relation="link". Those are span-link virtual edges (the call crosses a queue or a database and the two spans belong to different trace contexts), so they measure nothing. A mixed edge is measured over its non-link series only.

When present:

Field Meaning
rate RED family: requests per second over the window (always > 0). Schema-optional because data.metrics is a union
error_rate RED: failed fraction in [0, 1]. Absent when the failure counter could not be read; 0 when it was read and reported no failures — do not conflate the two
p90_server_ms RED: 90th percentile server-observed duration in milliseconds
read_ops / write_ops I/O family (pvc-to-netapp-aggr only): Harvest ops/s, verbatim
read_latency_us / write_latency_us I/O family: Harvest average latency in microseconds, verbatim
read_bytes_per_sec / write_bytes_per_sec I/O family: Harvest throughput in bytes per second, verbatim
max_iops / max_bytes_per_sec I/O family: declared QoS ceiling on the same edge. max_bytes_per_sec is qos_policy_fixed_max_throughput_mbps × 1048576. Neither field can appear without a measurement; absence means "no declared ceiling", never 0

Both new series are optional and degrade gracefully: a missing metric, empty result, or query error omits only the affected field (or leaves rate-only metrics) and never fails the build. All three values are JSON numbers rounded to 6 significant digits and may appear in exponent form (e.g. 3.86e-7 for one request over a 30-day window). Consumers must not assume fixed-decimal rendering (toFixed) and must treat 0 as semantically distinct from a very small non-zero value.

Producer prerequisites for RED coverage: the collector's dimensions must be identical across all three series — the failure counter and the histogram join the request counter by exact label identity, so a relabel or an extra dimension applied to only one family joins nothing (surfaced as its own warn: failed_total_label_set_mismatch / server_seconds_bucket_label_set_mismatch). add_metric_suffixes must be on so the histogram is named ..._server_seconds_bucket (not ..._server_bucket). Pod UIDs (client_k8s_pod_uid / server_k8s_pod_uid) are no longer required for measurement, but they give the most precise endpoint identity; multi-replica collectors need trace-ID-aware routing (loadbalancing exporter with routing_key: traceID) or client/server spans never pair, and an unpaired edge resolves through the peer-address ladder — still measured, but its _server_seconds is what the client observed, so p90_server_ms then includes network time the server never saw.

Query cost. The histogram is read raw (rate(..._bucket{...}[w]), no sum by), so roughly edge-cardinality × bucket-count series cross the wire. That is deliberate: no low-cardinality label subset identifies an edge once endpoints may come from peer addresses or connection strings, and a group-by would silently merge unrelated edges' latency distributions. The metric is optional — a store that refuses the query degrades exactly like an absent one (no p90_server_ms, no build failure).

The servicegraph connector's virtual peersclient="user" (an uninstrumented caller) and unknown (an unresolved peer) — are dropped at the query layer (client!~"user|unknown",server!~"user") and normally never appear as nodes or edges. The match is exact and case-sensitive, so a :// host that merely contains user is unaffected. The server side is narrowed to server!~"user" so a server="unknown" series still reaches the reader: when its client resolves to a real pod and the client-recorded peer address (client_server_address, then client_network_peer_address, then client_net_peer_name — first non-empty wins) names a Kubernetes Service, that peer is recovered into a pod-calls-service edge (or an external node for a non-cluster address) instead of being dropped; every other server="unknown" case is still dropped, byte-for-byte as before. The same sentinel fragment is applied to the two RED series selectors, which carry one matcher of their own — edge_relation!="link" (see the table above).

Probes — diagnostics, not graph data
PromQL Purpose
up Backs GET /readyz, and distinguishes "no data in window" (outside_retention) from "upstream healthy but window empty". Issued only for an unfiltered build — under any request filter, zero rows means "nothing in scope" and returns an empty 200. Not graph data.

The three traces_service_graph_* queries are also skipped when a filtered build loaded neither pods nor services — admission cannot keep any series, and those three are the one family no request matcher narrows.

Not VictoriaMetrics series, not graph-input metrics: the optional ClickHouse Istio route store (--route-store-dsn) resolves global FQDN peers to Services (off by default; a miss degrades to external); kube_state_graph_* are the API's own /metrics self-metrics.

Edge → metric mapping
Edge type Source metric(s)
pod-mounts-pvc kube_pod_spec_volumes_persistentvolumeclaims_info
pod-to-node kube_pod_info (node label; one per scheduled pod, intra-cluster)
pvc-to-netapp-aggr Harvest volume_labels joined on volume_name = PVC volumename (PV name)
pod-calls-pod traces_service_graph_request_total
pod-calls-service traces_service_graph_request_total (when target resolves to a service node via connection-string resolution)
service-selects-pod traces_service_graph_request_total (connection-string resolution + kube_endpointslice_* join)
Multi-cluster and cross-cluster coverage

Cross-cluster paths and service-graph scenarios are covered by internal/integration/ tests against a testcontainers-go VictoriaMetrics container. The suite spins up a real VictoriaMetrics, pushes hand-crafted fixture series via POST /api/v1/import/prometheus, and drives the in-process API — this is the sole verification path for multi-cluster, cross-cluster, and service-graph behaviour.

Configuration

Flag Env Default Notes
--prom-url KSG_PROM_URL http://localhost:8428 VictoriaMetrics Prometheus-compatible endpoint.
--listen-addr KSG_LISTEN_ADDR :8080 HTTP listen address.
--build-timeout KSG_BUILD_TIMEOUT 15s Per-build context timeout for /v1/graph.
--api-timeout KSG_API_TIMEOUT 5s Per-request timeout for upstream calls outside a graph build (/readyz probe, outside-retention probe).
--api-keys-file KSG_API_KEYS_FILE (empty) Path to a file holding accepted API keys (one per line, # comments allowed). Designed for K8s Secret mounts. Reloaded periodically.
--api-keys KSG_API_KEYS (empty) Comma-separated literal keys. Dev only; ignored when --api-keys-file is set.
--api-keys-reload-interval KSG_API_KEYS_RELOAD_INTERVAL 30s How often --api-keys-file is re-read. Set to 0 to disable hot reload.
--log-level KSG_LOG_LEVEL info debug | info | warn | error.
--az-label KSG_AZ_LABEL az Upstream label the ?az= parameter is matched against. The request parameter name never changes — only the label binding. Must be a valid PromQL label name and differ from --env-label.
--env-label KSG_ENV_LABEL env Upstream label the ?env= parameter is matched against.
KSG_PROM_USERNAME (empty) HTTP Basic Auth username for the upstream VictoriaMetrics endpoint. Env-only — no flag exists, because credential-carrying flags leak via ps and container specs. Must be set together with KSG_PROM_PASSWORD.
KSG_PROM_PASSWORD (empty) HTTP Basic Auth password for the upstream. Env-only, paired with KSG_PROM_USERNAME — setting exactly one of the two fails startup. Rotation requires a restart (no hot reload); changing a Secret-backed env var in a Deployment triggers a rollout anyway.
Upstream basic auth

When VictoriaMetrics is protected by basic auth (-httpAuth.*, vmauth, or an authenticating reverse proxy), set both env vars — in Kubernetes, source them from a Secret:

env:
  - name: KSG_PROM_USERNAME
    valueFrom:
      secretKeyRef: { name: ksg-upstream-auth, key: username }
  - name: KSG_PROM_PASSWORD
    valueFrom:
      secretKeyRef: { name: ksg-upstream-auth, key: password }

Every upstream request (topology, service-graph, cluster discovery, the /readyz probe) then carries Authorization: Basic …. The credential values never appear in logs, traces, metrics, or error responses.

Documentation

The full API reference is served by the running server:

The spec is generated from in-source annotations (make docs) and embedded into the binary, so it always matches the running build. The Scalar UI loads its front-end bundle from the jsDelivr CDN.

Development

First-time setup

Run once after cloning. Bootstraps the dev environment, downloads modules, and installs host-level tools (golangci-lint, govulncheck). Mockery is tracked via go.mod's tool directive (Go 1.24+) and invoked through go tool mockery — no separate install step is required.

make init           # go mod download + dev tools
make doctor         # verify toolchain (go, golangci-lint, govulncheck, mockery, docker)
make init-hooks     # (optional) install pre-commit hook (gofmt + go vet)

Required: Go 1.25+. The toolchain pinned in go.mod (currently go1.26.5) will be auto-fetched by Go on first build.

Day-to-day commands
make build          # compile binary
make test           # unit + component + golden + property + integration (Docker required)
make lint           # golangci-lint
make vuln           # govulncheck
make cover          # coverage profile
Mocks (mockery)

Production-side dependencies are exposed as small interfaces (promql.Querier, auth.Validator, clock.Clock) so unit tests can substitute mockery-generated mocks instead of fronting real services with httptest.NewServer. Mocks live under internal/<pkg>/mocks/ and are committed to git so CI does not need mockery installed.

make mocks          # regenerate mocks after editing an interface
make verify-mocks   # CI-style freshness check (regen + git diff)

.mockery.yaml lists the configured interfaces. After adding or editing any interface registered there, run make mocks and commit the regenerated files — the mocks-drift CI job blocks merges otherwise.

Test layout
Suite Where Real I/O?
Unit pkg/{graph,build,promql,clock,cytoscape,kubegraph}/*_test.go + internal/{config,auth,telemetry}/*_test.go None — pure Go.
Component internal/api/*_test.go None — MockQuerier injected via interface; httptest.NewServer only wraps the server-under-test, never fakes upstream.
Golden internal/api/golden_test.go + testdata/golden/*.json None. Run with -update to refresh snapshots.
Integration internal/integration/* Docker required. testcontainers-go spins a real VictoriaMetrics container; SkipIfDockerUnavailable skips locally without Docker. CI runs the full suite.

The boundary between unit and integration is strict: anything that touches a TCP socket fronting an upstream service is integration. Unit tests must run with no external dependencies.

License

Apache-2.0

Directories

Path Synopsis
cmd
kube-state-graph command
Command kube-state-graph runs the multi-cluster pod / node graph API server.
Command kube-state-graph runs the multi-cluster pod / node graph API server.
Package docs holds the generated OpenAPI specification, embedded into the binary so the API server can serve it with no external files at runtime.
Package docs holds the generated OpenAPI specification, embedded into the binary so the API server can serve it with no external files at runtime.
internal
api
Package api hosts the Gin HTTP handlers and middleware for the kube-state-graph v1 surface.
Package api hosts the Gin HTTP handlers and middleware for the kube-state-graph v1 surface.
auth
Package auth implements API-key authentication for the kube-state-graph HTTP API.
Package auth implements API-key authentication for the kube-state-graph HTTP API.
config
Package config holds the runtime configuration struct, flag/env binding, and validation for the kube-state-graph server.
Package config holds the runtime configuration struct, flag/env binding, and validation for the kube-state-graph server.
integration
Package integration contains testcontainers-driven integration tests.
Package integration contains testcontainers-driven integration tests.
observability
Package observability provides slog setup and Prometheus self-metric registration.
Package observability provides slog setup and Prometheus self-metric registration.
telemetry
Package telemetry wires the OpenTelemetry Go SDK into kube-state-graph.
Package telemetry wires the OpenTelemetry Go SDK into kube-state-graph.
pkg
build
Package build runs the topology + service-graph readers and joins their outputs into a multi-cluster *graph.Graph for a given time window.
Package build runs the topology + service-graph readers and joins their outputs into a multi-cluster *graph.Graph for a given time window.
clock
Package clock provides a tiny Clock interface used by handlers and the builder to evaluate "now".
Package clock provides a tiny Clock interface used by handlers and the builder to evaluate "now".
cytoscape
Package cytoscape serialises a built graph.Graph (projected to a graph.View) into the deterministic Cytoscape.js response body served at /v1/graph.
Package cytoscape serialises a built graph.Graph (projected to a graph.View) into the deterministic Cytoscape.js response body served at /v1/graph.
graph
Package graph defines the in-memory graph types (nodes, edges, adjacency) shared by the build pipeline, projection layer, and HTTP serialisers.
Package graph defines the in-memory graph types (nodes, edges, adjacency) shared by the build pipeline, projection layer, and HTTP serialisers.
internal/testlog
Package testlog provides the shared slog-capture helper for pkg/ unit tests that assert on global-logger output (the engine's warn paths log via the process-wide slog default; see the D32 note in CLAUDE.md).
Package testlog provides the shared slog-capture helper for pkg/ unit tests that assert on global-logger output (the engine's warn paths log via the process-wide slog default; see the D32 note in CLAUDE.md).
kubegraph
Package kubegraph is the convenience facade over the reusable graph engine.
Package kubegraph is the convenience facade over the reusable graph engine.
promql
Package promql wraps the Prometheus HTTP API client and centralises the PromQL query templates used by the build pipeline.
Package promql wraps the Prometheus HTTP API client and centralises the PromQL query templates used by the build pipeline.
promql/backendsfile
Package backendsfile reads the upstream routing table from the file an operator mounts, and produces the validated promql.Table the router dispatches through.
Package backendsfile reads the upstream routing table from the file an operator mounts, and produces the validated promql.Table the router dispatches through.
route
Package route is the concrete build.RouteResolver: the Istio route-resolution engine that answers "which Kubernetes Service did the ingress config route (host, path, port) to at instant `at`?" against a versioned config store written by the metadata-exporter.
Package route is the concrete build.RouteResolver: the Istio route-resolution engine that answers "which Kubernetes Service did the ingress config route (host, path, port) to at instant `at`?" against a versioned config store written by the metadata-exporter.
route/gwresolve
Package gwresolve finds the ingress gateway serving a request host by matching the host against every gateway's server hosts, using Istio's own wildcard/most-specific semantics — exactly like real Istio gateway selection.
Package gwresolve finds the ingress gateway serving a request host by matching the host against every gateway's server hosts, using Istio's own wildcard/most-specific semantics — exactly like real Istio gateway selection.
route/matchcheck
Package matchcheck drives Envoy's router_check_tool (offline, no traffic, no Envoy process) over istiod-translated RouteConfigurations to resolve each host+path to its destination cluster.
Package matchcheck drives Envoy's router_check_tool (offline, no traffic, no Envoy process) over istiod-translated RouteConfigurations to resolve each host+path to its destination cluster.
route/snapshot
Package snapshot resolves a store.TrafficSnapshot in memory.
Package snapshot resolves a store.TrafficSnapshot in memory.
route/store
Package store is the READ-ONLY contract for the versioned Istio-config store backing route resolution (the translate-global-fqdn-to-k8s-service change).
Package store is the READ-ONLY contract for the versioned Istio-config store backing route resolution (the translate-global-fqdn-to-k8s-service change).
route/translate
Package translate turns one ingress gateway's scoped Istio config (its single Gateway CR + the VirtualServices bound to it + the Services those VS route to) into the Envoy RouteConfiguration istiod would push to that gateway for a given listener port.
Package translate turns one ingress gateway's scoped Istio config (its single Gateway CR + the VirtualServices bound to it + the Services those VS route to) into the Envoy RouteConfiguration istiod would push to that gateway for a given listener port.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL