Skip to content
Merged
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
23 changes: 21 additions & 2 deletions apps/ai/src/mcp/__evals__/infra-world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,26 @@ const PODS: ReadonlyArray<Pod> = [
pod("otel-agent-q4w9e", "observability", "ip-10-0-3-57", ["daemonset", "otel-agent"], 0.27, 0, () => 0),
]

const GIB = 1024 ** 3
const NODES = [
{ nodeName: "ip-10-0-3-21", nodeUid: "4f1c-21", kubeletVersion: "v1.31.2", uptime: 1_814_400 },
{ nodeName: "ip-10-0-3-57", nodeUid: "9a7e-57", kubeletVersion: "v1.31.2", uptime: 86_400 },
{
nodeName: "ip-10-0-3-21",
nodeUid: "4f1c-21",
kubeletVersion: "v1.31.2",
uptime: 1_814_400,
cpuAllocatable: 3.92,
memoryUsage: 9.4 * GIB,
memoryAllocatable: 14.6 * GIB,
},
{
nodeName: "ip-10-0-3-57",
nodeUid: "9a7e-57",
kubeletVersion: "v1.31.2",
uptime: 86_400,
cpuAllocatable: 3.92,
memoryUsage: 11.8 * GIB,
memoryAllocatable: 14.6 * GIB,
},
]
/** System daemons outside any pod. */
const NODE_OVERHEAD_CORES = 0.3
Expand Down Expand Up @@ -641,6 +658,8 @@ export const infraFixtureRules = (): FixtureRule[] => [
(n) => ({
...n,
cpuUsage: avgOf(nodeCpu(n.nodeName)),
cpuUtilization: avgOf(nodeCpu(n.nodeName)) / n.cpuAllocatable,
memoryUtilization: n.memoryUsage / n.memoryAllocatable,
clusterName: INFRA_WORLD.cluster,
environment: INFRA_WORLD.environment,
containerRuntime: "containerd://1.7.22",
Expand Down
16 changes: 12 additions & 4 deletions apps/api/src/routes/internal/query-engine.http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,16 @@ const toReleaseTimelinePoint = (row: CH.ReleasesTimelineOutput) => ({
count: row.count,
})

const nodeCapacity = (row: CH.NodeCapacityOutput) => ({
cpuUsage: row.cpuUsage || 0,
cpuAllocatable: row.cpuAllocatable || 0,
cpuUtilization: row.cpuUtilization || 0,
memoryUsage: row.memoryUsage || 0,
memoryAllocatable: row.memoryAllocatable || 0,
memoryUtilization: row.memoryUtilization || 0,
uptime: row.uptime || 0,
})

const toServiceWorkloadRow = (row: CH.ServiceWorkloadsOutput) => ({
serviceName: decodeServiceName(row.serviceName),
workloadKind: row.workloadKind,
Expand Down Expand Up @@ -1741,8 +1751,7 @@ export const HttpQueryEngineLive = HttpApiBuilder.group(MapleInternalApi, "query
environment: row.environment,
kubeletVersion: row.kubeletVersion,
lastSeen: DateTime.formatIso(row.lastSeen),
cpuUsage: row.cpuUsage || 0,
uptime: row.uptime || 0,
...nodeCapacity(row),
})),
})
}),
Expand All @@ -1760,8 +1769,7 @@ export const HttpQueryEngineLive = HttpApiBuilder.group(MapleInternalApi, "query
containerRuntime: row.containerRuntime,
firstSeen: DateTime.formatIso(row.firstSeen),
lastSeen: DateTime.formatIso(row.lastSeen),
cpuUsage: row.cpuUsage || 0,
uptime: row.uptime || 0,
...nodeCapacity(row),
}
: null,
})
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/routes/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ export const Queries = {
CH.compile(
CH.nodeGaugeTimeseriesQuery({
nodeName: payload.nodeName,
metricName: nodeMetricSpec(payload.metric).metricName,
...nodeMetricSpec(payload.metric),
}),
{
orgId,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/api/warehouse/infra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ export function nodeDetailSummary({ data }: { data: NodeDetailSummaryInput }) {
)
}

export type NodeInfraMetric = "cpu_usage" | "uptime"
export type NodeInfraMetric = NodeInfraTimeseriesRequest["metric"]

export interface NodeInfraTimeseriesInput {
startTime: string
Expand Down
28 changes: 27 additions & 1 deletion apps/web/src/components/infra/format.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { describe, expect, it } from "vitest"
import { deriveHostStatus, severityLevel } from "./format"
import {
capacityFraction,
deriveHostStatus,
formatBytesOfAllocatable,
formatCoresOfAllocatable,
severityLevel,
} from "./format"
import { formatBytesPerSecond, formatLoad, formatPercent, formatUptime } from "@maple/ui/lib/format"

describe("deriveHostStatus", () => {
Expand Down Expand Up @@ -107,3 +113,23 @@ describe("severityLevel", () => {
expect(severityLevel(Number.NaN)).toBe("ok")
})
})

describe("node capacity", () => {
it("keeps the utilization when allocatable was collected", () => {
expect(capacityFraction(0.25, 4)).toBe(0.25)
})

it("is NaN when allocatable was not collected, so it renders a dash", () => {
expect(capacityFraction(0, 0)).toBeNaN()
})

it("formats cores against allocatable", () => {
expect(formatCoresOfAllocatable(0.4231, 3.92)).toBe("0.42 / 3.92 cores")
expect(formatCoresOfAllocatable(0.4231, 0)).toBe("0.42 cores")
})

it("formats memory against allocatable", () => {
expect(formatBytesOfAllocatable(3 * 1024 ** 3, 16 * 1024 ** 3)).toBe("3.0 GB / 16.0 GB")
expect(formatBytesOfAllocatable(3 * 1024 ** 3, 0)).toBe("3.0 GB")
})
})
19 changes: 18 additions & 1 deletion apps/web/src/components/infra/format.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EMPTY_VALUE } from "@maple/ui/lib/format"
import { EMPTY_VALUE, formatBytes } from "@maple/ui/lib/format"
import { toEpochMs } from "@maple/ui/lib/time-format"
import { type UtilizationLevel, utilizationLevel } from "@maple/ui/lib/utilization"

Expand Down Expand Up @@ -40,3 +40,20 @@ export const formatWholePercent = (fraction: number) =>

/** CPU usage in cores, two decimals. */
export const formatCores = (cores: number) => (Number.isFinite(cores) ? cores.toFixed(2) : EMPTY_VALUE)

/**
* A node's usage / allocatable fraction, or NaN when allocatable was not collected
* (the API sends 0 for both), so meters and percents render a dash instead of 0%.
*/
export const capacityFraction = (utilization: number, allocatable: number) =>
allocatable > 0 ? utilization : Number.NaN

/** "0.42 / 3.92 cores"; usage alone when allocatable was not collected. */
export const formatCoresOfAllocatable = (usage: number, allocatable: number) =>
allocatable > 0
? `${formatCores(usage)} / ${formatCores(allocatable)} cores`
: `${formatCores(usage)} cores`

/** "3.1 GB / 15.6 GB"; usage alone when allocatable was not collected. */
export const formatBytesOfAllocatable = (usage: number, allocatable: number) =>
allocatable > 0 ? `${formatBytes(usage)} / ${formatBytes(allocatable)}` : formatBytes(usage)
3 changes: 2 additions & 1 deletion apps/web/src/components/infra/infra-correlations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ const POD_CHARTS: ReadonlyArray<{ label: string; metric: PodInfraMetric }> = [

const NODE_CHARTS: ReadonlyArray<{ label: string; metric: NodeInfraMetric }> = [
{ label: "CPU cores", metric: "cpu_usage" },
{ label: "Uptime", metric: "uptime" },
{ label: "CPU / allocatable", metric: "cpu_utilization" },
{ label: "Memory / allocatable", metric: "memory_utilization" },
]

const HOST_CHARTS: ReadonlyArray<{ label: string; metric: HostInfraMetric }> = [
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/components/infra/k8s-detail-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const POD_METRIC_LABELS: Record<PodInfraMetric, string> = {

const NODE_METRIC_LABELS: Record<NodeInfraMetric, string> = {
cpu_usage: "CPU usage",
cpu_utilization: "CPU / allocatable",
memory_usage: "Memory working set",
memory_utilization: "Memory / allocatable",
uptime: "Uptime",
} satisfies Record<NodeInfraMetric, string>

Expand Down
47 changes: 37 additions & 10 deletions apps/web/src/components/infra/node-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ import {
useTableSort,
} from "@/components/common/data-table"
import { MetaLine } from "./primitives/meta-line"
import { formatCores } from "./format"
import { MeterRows } from "./primitives/meter-rows"
import { capacityFraction, formatBytesOfAllocatable, formatCoresOfAllocatable } from "./format"
import { formatUptime } from "@maple/ui/lib/format"
import { RelativeTime } from "@/components/common/relative-time"

export type NodeRow = ListNodesResponse["data"][number]

type SortKey = "nodeName" | "cpuUsage" | "uptime" | "lastSeen"
type SortKey = "nodeName" | "cpuUtilization" | "memoryUtilization" | "uptime" | "lastSeen"

interface NodeTableProps {
nodes: ReadonlyArray<NodeRow>
Expand All @@ -34,13 +35,19 @@ function NodeColumns({ sort }: { sort?: SortControls<SortKey> }) {
<>
<ColumnHead<SortKey> label="Node" sortKey="nodeName" {...sort} width="w-0 flex-1 min-w-[260px]" />
<ColumnHead<SortKey>
label="CPU cores"
sortKey="cpuUsage"
label="CPU / allocatable"
sortKey="cpuUtilization"
{...sort}
align="right"
width="w-[110px]"
width="w-[176px]"
hidden="hidden md:flex"
/>
<ColumnHead<SortKey>
label="Memory / allocatable"
sortKey="memoryUtilization"
{...sort}
width="w-[176px]"
hidden="hidden lg:flex"
/>
<ColumnHead<SortKey>
label="Uptime"
sortKey="uptime"
Expand Down Expand Up @@ -71,7 +78,8 @@ export function NodeTableLoading() {
<Skeleton className="h-4 w-48" />
<Skeleton className="mt-1.5 h-3 w-32" />
</div>
<Skeleton className="hidden h-3 w-[110px] md:block" />
<Skeleton className="hidden h-3 w-[176px] md:block" />
<Skeleton className="hidden h-3 w-[176px] lg:block" />
<Skeleton className="hidden h-3 w-[100px] md:block" />
<Skeleton className="h-3 w-[100px]" />
</DataTable.SkeletonRows>
Expand All @@ -81,7 +89,7 @@ export function NodeTableLoading() {

export function NodeTable({ nodes, waiting, referenceTime }: NodeTableProps) {
const { sorted, sortKey, sortDir, handleSort } = useTableSort<NodeRow, SortKey>(nodes, {
initialKey: "cpuUsage",
initialKey: "cpuUtilization",
stringKeys: ["nodeName"],
})

Expand Down Expand Up @@ -110,8 +118,17 @@ export function NodeTable({ nodes, waiting, referenceTime }: NodeTableProps) {
</div>
<MetaLine items={[node.kubeletVersion && `kubelet ${node.kubeletVersion}`]} />
</div>
<div className="hidden w-[110px] text-right font-mono text-xs tabular-nums text-foreground/80 md:block">
{formatCores(node.cpuUsage)}
<div className="hidden w-[176px] md:block">
<CapacityCell
fraction={capacityFraction(node.cpuUtilization, node.cpuAllocatable)}
caption={formatCoresOfAllocatable(node.cpuUsage, node.cpuAllocatable)}
/>
</div>
<div className="hidden w-[176px] lg:block">
<CapacityCell
fraction={capacityFraction(node.memoryUtilization, node.memoryAllocatable)}
caption={formatBytesOfAllocatable(node.memoryUsage, node.memoryAllocatable)}
/>
</div>
<div className="hidden w-[100px] text-right font-mono text-xs tabular-nums text-foreground/80 md:block">
{formatUptime(node.uptime)}
Expand All @@ -128,3 +145,13 @@ export function NodeTable({ nodes, waiting, referenceTime }: NodeTableProps) {
</DataTable.Root>
)
}

/** Usage against allocatable capacity: the meter answers "is this node full", the caption says of what. */
function CapacityCell({ fraction, caption }: { fraction: number; caption: string }) {
return (
<div className="space-y-1">
<MeterRows hideLabels meters={[{ label: "usage", fraction }]} />
<div className="truncate font-mono text-2xs tabular-nums text-muted-foreground">{caption}</div>
</div>
)
}
49 changes: 41 additions & 8 deletions apps/web/src/routes/infra/kubernetes/nodes/$nodeName.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ import {
ResourceAttributesCardSkeleton,
} from "@/components/infra/primitives/resource-attributes-card"
import { NoMetricsMessage } from "@/components/infra/primitives/no-metrics-message"
import { EMPTY_VALUE, formatUptime } from "@maple/ui/lib/format"
import { EMPTY_VALUE, formatPercent, formatUptime } from "@maple/ui/lib/format"

import type { NodeInfraMetric } from "@/api/warehouse/infra"
import { ServerIcon } from "@/components/icons"
import { KubernetesShell } from "@/components/infra/kubernetes/kubernetes-shell"
import { NodeDetailChart } from "@/components/infra/k8s-detail-chart"
import { PodTable } from "@/components/infra/pod-table"
import { bucketSecondsForRange } from "@/components/infra/constants"
import { formatCores } from "@/components/infra/format"
import {
capacityFraction,
formatBytesOfAllocatable,
formatCoresOfAllocatable,
severityLevel,
} from "@/components/infra/format"
import { PageHero, HeroChip } from "@/components/common/page-hero"
import { SegmentPivot } from "@/components/infra/primitives/segment-pivot"
import { StatRail, StatRailItem } from "@/components/common/stat-rail"
Expand All @@ -44,6 +49,9 @@ export const Route = createFileRoute("/infra/kubernetes/nodes/$nodeName")({

const METRIC_OPTIONS = [
{ value: "cpu_usage", label: "CPU cores" },
{ value: "cpu_utilization", label: "CPU / allocatable" },
{ value: "memory_usage", label: "Memory" },
{ value: "memory_utilization", label: "Memory / allocatable" },
{ value: "uptime", label: "Uptime" },
] as const satisfies ReadonlyArray<{ value: NodeInfraMetric; label: string }>

Expand Down Expand Up @@ -101,7 +109,7 @@ function NodeDetailPage() {
<div className="space-y-6">
<PageHero
title={<span className="font-mono">{nodeName}</span>}
description="Node metrics from the kubelet stats receiver."
description="Usage from the kubelet stats receiver, allocatable capacity from the cluster receiver."
meta={
summary ? (
<>
Expand All @@ -117,11 +125,16 @@ function NodeDetailPage() {
/>

{summary ? (
<StatRail columns={3}>
<StatRailItem
eyebrow="CPU cores"
value={formatCores(summary.cpuUsage)}
compact
<StatRail columns={4}>
<CapacityStat
eyebrow="CPU / allocatable"
fraction={capacityFraction(summary.cpuUtilization, summary.cpuAllocatable)}
subline={formatCoresOfAllocatable(summary.cpuUsage, summary.cpuAllocatable)}
/>
<CapacityStat
eyebrow="Memory / allocatable"
fraction={capacityFraction(summary.memoryUtilization, summary.memoryAllocatable)}
subline={formatBytesOfAllocatable(summary.memoryUsage, summary.memoryAllocatable)}
/>
<StatRailItem eyebrow="Uptime" value={formatUptime(summary.uptime)} compact />
<StatRailItem
Expand Down Expand Up @@ -176,3 +189,23 @@ function NodeDetailPage() {
</KubernetesShell>
)
}

function CapacityStat({
eyebrow,
fraction,
subline,
}: {
eyebrow: string
fraction: number
subline: string
}) {
return (
<StatRailItem
eyebrow={eyebrow}
value={formatPercent(fraction)}
tone={severityLevel(fraction)}
subline={subline}
compact
/>
)
}
2 changes: 2 additions & 0 deletions deploy/k8s-infra/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ presets:
- DiskPressure
- PIDPressure
- NetworkUnavailable
# Emits k8s.node.allocatable_cpu (cores) and k8s.node.allocatable_memory (bytes);
# the Nodes view divides kubelet usage by these for CPU/memory utilization.
allocatableTypesToReport:
- cpu
- memory
Expand Down
Loading
Loading