model

package
v0.1.21 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 2026 License: MIT Imports: 2 Imported by: 0

Documentation

Overview

Package model holds the plain-data types shared by the gather, detect and tool layers.

Nothing here may reference a Kubernetes client, a context, or a live API object: Snapshot must round-trip through YAML so that `argus capture` writes fixtures, tests read them back, and production runs the identical detect path. That constraint is load-bearing — see the plan.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ParseCPU

func ParseCPU(q string) int64

ParseCPU returns a CPU quantity in millicores, or 0 if unset/unparseable. Detectors do arithmetic on these, so a bad value must degrade to "unknown", never panic.

func ParseMem

func ParseMem(q string) int64

ParseMem returns a memory quantity in bytes, or 0 if unset/unparseable.

func Wrap added in v0.1.9

func Wrap(s string, indent int) string

Wrap reflows text to ~92 columns with the given indent.

Lives here because both the diagnosis renderer and the log renderer need it, and a copy in each is how two renderers start disagreeing about their own output. Long unwrapped lines are not cosmetic: an evidence line ran to 181 characters, which wraps unreadably in a terminal and forced horizontal scrolling everywhere else it was shown.

Types

type ChainPod added in v0.1.19

type ChainPod struct {
	Name  string `json:"name"`
	Ready bool   `json:"ready"`
	Phase string `json:"phase,omitempty"`
	// PortNames and PortNumbers are what the containers DECLARE. Kubernetes neither
	// requires a listening port to be declared nor verifies that a declared one is
	// listening, which is why a numeric mismatch is a hint and a named one is proof.
	PortNames   []string `json:"port_names,omitempty"`
	PortNumbers []int32  `json:"port_numbers,omitempty"`
}

ChainPod is a pod the selector matched, with the ports its containers declare.

type ConditionView

type ConditionView struct {
	Type              string `json:"type"`
	Status            string `json:"status"`
	Reason            string `json:"reason,omitempty"`
	Message           string `json:"message,omitempty"`
	LastChangeSecsAgo int64  `json:"last_change_seconds_ago,omitempty"`
}

ConditionView is a k8s condition reduced to what a detector reads.

type ContainerSpecView

type ContainerSpecView struct {
	Name  string `json:"name"`
	Image string `json:"image"`

	RequestCPU string `json:"request_cpu,omitempty"`
	RequestMem string `json:"request_mem,omitempty"`
	LimitCPU   string `json:"limit_cpu,omitempty"`
	LimitMem   string `json:"limit_mem,omitempty"`

	// Command is the entrypoint override. It is what exit 126/127 is usually about, and citing it
	// turns "exit 127" into "your command is X and the image does not contain it".
	Command []string `json:"command,omitempty"`
	Args    []string `json:"args,omitempty"`
	EnvKeys []string `json:"env_keys,omitempty"` // keys only; values are redacted at the projection boundary
	EnvFrom []string `json:"env_from,omitempty"`
	Mounts  []string `json:"mounts,omitempty"`

	Readiness *ProbeView `json:"readiness,omitempty"`
	Liveness  *ProbeView `json:"liveness,omitempty"`
	Startup   *ProbeView `json:"startup,omitempty"`
}

ContainerSpecView is the desired shape of a container — the half of ContainerView that also appears in a ReplicaSet template, which is why it is factored out and diffable on its own.

type ContainerStateView

type ContainerStateView struct {
	Status     string `json:"status"` // running | waiting | terminated
	Reason     string `json:"reason,omitempty"`
	Message    string `json:"message,omitempty"`
	ExitCode   int32  `json:"exit_code,omitempty"`
	Signal     int32  `json:"signal,omitempty"`
	SecondsAgo int64  `json:"seconds_ago,omitempty"` // since started or finished
}

ContainerStateView flattens running/waiting/terminated into one shape.

type ContainerView

type ContainerView struct {
	ContainerSpecView `json:",inline"`

	Ready        bool  `json:"ready"`
	Started      bool  `json:"started"`
	RestartCount int32 `json:"restart_count"`

	State     ContainerStateView  `json:"state"`
	LastState *ContainerStateView `json:"last_state,omitempty"`

	// UsageCPU/UsageMem come from metrics.k8s.io; empty when the metrics API is unavailable.
	UsageCPU string `json:"usage_cpu,omitempty"`
	UsageMem string `json:"usage_mem,omitempty"`
}

ContainerView merges the container spec, its live status, and its current metrics — the three things every detector needs to correlate and which kubectl makes you fetch separately.

type EventGroup

type EventGroup struct {
	Type       string `json:"type"` // Normal | Warning
	Reason     string `json:"reason"`
	Message    string `json:"message"` // normalized: UIDs, IPs, digests stripped
	Count      int32  `json:"count"`   // total occurrences, respecting the apiserver's own aggregation
	ObjectKind string `json:"object_kind,omitempty"`
	ObjectName string `json:"object_name,omitempty"` // one example object, not the only one
	// ObjectCount is how many distinct objects reported this. Forty pods reporting the same
	// BackOff is one group with ObjectCount 40, which is the fact an SRE actually wants.
	ObjectCount         int   `json:"object_count,omitempty"`
	FirstSeenSecondsAgo int64 `json:"first_seen_seconds_ago"`
	LastSeenSecondsAgo  int64 `json:"last_seen_seconds_ago"`
}

EventGroup is a deduplicated bucket of events. A hundred BackOff events collapse to one line.

type Evidence

type Evidence struct {
	Source  string `json:"source"`  // "pod.lastState", "event", "replicaset.diff", "metrics"
	Ref     string `json:"ref"`     // "pod/checkout-api-7d9f", "rs/checkout-api-7d9f"
	Excerpt string `json:"excerpt"` // the actual value that triggered the detector
}

Evidence is a citation. Every Finding must carry at least one — a high-confidence diagnosis with nothing to check is worse than no diagnosis during an incident.

type Finding

type Finding struct {
	ID       string   `json:"id"`       // "oomkill.limit-too-low"
	Severity Severity `json:"severity"` // critical | warning | info

	// Confidence is how sure we are the finding is real, independent of severity. Be honest: a
	// detector working from partial data (see Snapshot.Degraded) must dock its own confidence.
	Confidence float64 `json:"confidence"` // 0.0 - 1.0

	Scope    string     `json:"scope"`    // "workload/checkout-api", "node/gke-abc"
	Title    string     `json:"title"`    // one line
	Detail   string     `json:"detail"`   // 2-4 sentences: cause, not symptom
	Evidence []Evidence `json:"evidence"` // never empty; enforced by test
	NextTool *ToolHint  `json:"next_tool,omitempty"`

	// Suppresses lists finding IDs this one subsumes. The node detector uses it to widen scope:
	// three Deployments failing on one node is one node finding, not three workload diagnoses.
	Suppresses []string `json:"-"`
}

Finding is one ranked diagnosis. Flat and JSON-friendly, matching the gospect-mcp finding model.

func (Finding) Less

func (f Finding) Less(g Finding) int

Less reports whether f should sort before g: severity desc, then confidence desc, then ID for a stable order. Used with slices.SortStableFunc, so equal elements keep registry order.

type HPAView

type HPAView struct {
	Name        string          `json:"name"`
	MinReplicas int32           `json:"min_replicas"`
	MaxReplicas int32           `json:"max_replicas"`
	Current     int32           `json:"current_replicas"`
	Desired     int32           `json:"desired_replicas"`
	Conditions  []ConditionView `json:"conditions,omitempty"`
}

HPAView explains scale decisions that look like workload failures.

type Hop added in v0.1.19

type Hop struct {
	Step string `json:"step"`
	// Status is ok | broken | warn | skipped. "skipped" covers both a hop that does not
	// apply and one downstream of a break, where the data says nothing about health.
	Status string `json:"status"`
	Detail string `json:"detail"`
	// Remedy is set only on the hop where the chain breaks.
	Remedy string `json:"remedy,omitempty"`
}

Hop is one link in the chain, in the order traffic traverses it.

type IngressRoute added in v0.1.19

type IngressRoute struct {
	Ingress string `json:"ingress"`
	Class   string `json:"class,omitempty"`
	Host    string `json:"host,omitempty"`
	Path    string `json:"path,omitempty"`
	// BackendPort as written on the rule: a number, or a Service port NAME.
	BackendPort   string `json:"backend_port"`
	BackendIsName bool   `json:"backend_is_name,omitempty"`
}

IngressRoute is one Ingress rule pointing at the Service being traced.

type LogBundle added in v0.1.2

type LogBundle struct {
	Pod       string `json:"pod"`
	Container string `json:"container"`
	// Previous reports whether these are the *previous* container instance's logs. On a
	// crashlooping pod that is almost always what you want: the current instance is in backoff
	// and has produced nothing.
	Previous bool `json:"previous"`
	// Reason explains why this pod, container and instance were chosen, so the selection is
	// auditable rather than magic.
	Reason string `json:"reason"`

	Groups []LogGroup `json:"groups"`

	// DroppedGroups counts distinct lines elided by the token budget. Never silent: a truncated
	// log that does not say it was truncated reads as a complete one.
	DroppedGroups int    `json:"dropped_groups,omitempty"`
	Note          string `json:"note,omitempty"`
}

LogBundle is projected container output: redacted, grouped and budgeted.

Raw logs are the least structured and most dangerous thing argus emits — unbounded in size, and the one place an application can print a credential. Everything here exists to bound one of those two risks.

type LogGroup added in v0.1.2

type LogGroup struct {
	Text  string `json:"text"`
	Count int    `json:"count"`
	// FirstSecondsAgo/LastSecondsAgo are zero when the log carried no usable timestamps.
	FirstSecondsAgo int64 `json:"first_seconds_ago,omitempty"`
	LastSecondsAgo  int64 `json:"last_seconds_ago,omitempty"`
}

LogGroup is a set of log lines identical after normalization. Ten thousand identical panics collapse to one entry with a count, which is the difference between a readable diagnosis and a context window full of the same stack trace.

type NodeCapacity added in v0.1.16

type NodeCapacity struct {
	Name          string            `json:"name"`
	Labels        map[string]string `json:"labels,omitempty"`
	Taints        []Taint           `json:"taints,omitempty"`
	Unschedulable bool              `json:"unschedulable,omitempty"`
	Ready         bool              `json:"ready"`

	AllocCPUMilli int64 `json:"alloc_cpu_milli"`
	AllocMemBytes int64 `json:"alloc_mem_bytes"`
	// Used is the sum of REQUESTS of pods already assigned here, which is what the
	// scheduler reserves against — not current usage, which is a different number and
	// the one people reach for by mistake.
	UsedCPUMilli int64 `json:"used_cpu_milli"`
	UsedMemBytes int64 `json:"used_mem_bytes"`
}

NodeCapacity is one node's schedulable state: what it has, what is already committed on it, and what would keep a pod off it.

type NodeFit added in v0.1.16

type NodeFit struct {
	Node string `json:"node"`
	Fits bool   `json:"fits"`
	// Reasons is empty when the node fits. Each entry names the check and its numbers,
	// because "insufficient memory" without the figures is not an explanation.
	Reasons []string `json:"reasons,omitempty"`

	FreeCPUMilli int64 `json:"free_cpu_milli"`
	FreeMemBytes int64 `json:"free_mem_bytes"`
}

NodeFit is the verdict for one node, with the numbers that produced it.

type NodeView

type NodeView struct {
	Name          string          `json:"name"`
	Ready         bool            `json:"ready"`
	Unschedulable bool            `json:"unschedulable,omitempty"`
	Conditions    []ConditionView `json:"conditions,omitempty"`
	Taints        []string        `json:"taints,omitempty"`
	AllocCPU      string          `json:"alloc_cpu,omitempty"`
	AllocMem      string          `json:"alloc_mem,omitempty"`
}

NodeView carries the conditions that explain workload failures the workload did not cause.

type PDBView

type PDBView struct {
	Name               string `json:"name"`
	DesiredHealthy     int32  `json:"desired_healthy"`
	CurrentHealthy     int32  `json:"current_healthy"`
	DisruptionsAllowed int32  `json:"disruptions_allowed"`
}

PDBView explains rollouts that are stuck rather than broken.

type PendingReport added in v0.1.16

type PendingReport struct {
	Pod            string `json:"pod"`
	Namespace      string `json:"namespace"`
	PendingSeconds int64  `json:"pending_seconds"`
	// Reason/Message are the scheduler's own words from the PodScheduled condition,
	// reported alongside our arithmetic rather than instead of it.
	Reason  string `json:"scheduler_reason,omitempty"`
	Message string `json:"scheduler_message,omitempty"`

	Spec  PendingSpec `json:"asked_for"`
	Nodes []NodeFit   `json:"nodes"`

	Feasible int      `json:"feasible_nodes"`
	Summary  []string `json:"summary"`
	// NotChecked states the limits of the analysis. A scheduling explanation that
	// implies completeness it does not have is worse than one that names its gaps.
	NotChecked []string `json:"not_checked,omitempty"`
}

PendingReport explains one pod's unschedulability.

type PendingSpec added in v0.1.16

type PendingSpec struct {
	NeedCPUMilli int64             `json:"need_cpu_milli"`
	NeedMemBytes int64             `json:"need_mem_bytes"`
	NodeSelector map[string]string `json:"node_selector,omitempty"`
	Tolerations  []Toleration      `json:"tolerations,omitempty"`
	HasAffinity  bool              `json:"has_node_affinity,omitempty"`
}

PendingSpec is what the unschedulable pod is asking for.

type PodView

type PodView struct {
	Name      string            `json:"name"`
	Node      string            `json:"node,omitempty"`
	Phase     string            `json:"phase"`
	Ready     bool              `json:"ready"`
	Labels    map[string]string `json:"labels,omitempty"`
	OwnerKind string            `json:"owner_kind,omitempty"`
	OwnerName string            `json:"owner_name,omitempty"`

	CreatedSecondsAgo int64 `json:"created_seconds_ago"`

	// SchedulingReason/Message come from the PodScheduled condition when Phase is Pending.
	SchedulingReason  string `json:"scheduling_reason,omitempty"`
	SchedulingMessage string `json:"scheduling_message,omitempty"`

	Containers []ContainerView `json:"containers,omitempty"`
}

PodView is the projected pod. Budget: under 400 tokens serialized — enforced by test.

type ProbeView

type ProbeView struct {
	Kind             string `json:"kind"` // http | tcp | exec | grpc
	InitialDelay     int32  `json:"initial_delay"`
	Period           int32  `json:"period"`
	Timeout          int32  `json:"timeout"`
	FailureThreshold int32  `json:"failure_threshold"`
	SuccessThreshold int32  `json:"success_threshold,omitempty"`
}

ProbeView is the probe timing a detector reasons about; the handler details do not matter to it.

func (*ProbeView) Deadline

func (p *ProbeView) Deadline() int32

Deadline is how long the probe tolerates a slow start before the kubelet acts, in seconds. This is the number the readiness detector compares against observed startup time — computing it by hand from four separate fields is exactly the arithmetic humans get wrong at 3am.

type ReplicaSetView

type ReplicaSetView struct {
	Name              string `json:"name"`
	Revision          string `json:"revision,omitempty"`
	Desired           int32  `json:"desired"`
	Ready             int32  `json:"ready"`
	Available         int32  `json:"available"`
	Current           bool   `json:"current"` // matches the workload's current pod-template hash
	CreatedSecondsAgo int64  `json:"created_seconds_ago"`

	// Template is the projected pod template, which is what the rollout detector diffs.
	Template []ContainerSpecView `json:"template,omitempty"`
}

ReplicaSetView carries enough of a ReplicaSet to diff two generations and tell which one is the current rollout target.

type ServiceChain added in v0.1.19

type ServiceChain struct {
	Service   string            `json:"service"`
	Namespace string            `json:"namespace"`
	Type      string            `json:"type,omitempty"`
	Selector  map[string]string `json:"selector,omitempty"`
	Ports     []ServicePortSpec `json:"ports,omitempty"`
	Routes    []IngressRoute    `json:"routes,omitempty"`
	Matched   []ChainPod        `json:"matched_pods,omitempty"`

	// NearMiss names pods whose label value is a plausible misspelling of what the selector
	// wants. That is the shape a mislabelled workload has, and it is the whole difference
	// between "nothing is deployed here" and "your selector has a typo".
	//
	// Plausible is load-bearing. Matching on a shared label KEY looks equivalent and
	// over-matches badly: every pod in a namespace carries `app`, so on a live cluster it
	// named six unrelated workloads and pointed the reader at the wrong one.
	NearMiss []string `json:"near_miss_pods,omitempty"`
	// NearMissTotal is how many pods qualified, which is not len(NearMiss) — that list is
	// truncated for readability, and reporting its length as the count states a wrong number.
	NearMissTotal int `json:"near_miss_total,omitempty"`

	EndpointsReady    int `json:"endpoints_ready"`
	EndpointsNotReady int `json:"endpoints_not_ready"`
	// EndpointPorts is what the EndpointSlices actually carry. Empty while pods matched is
	// the dataplane confirming a targetPort that resolved to nothing.
	EndpointPorts []int32 `json:"endpoint_ports,omitempty"`

	ExternalPolicyLocal bool `json:"external_traffic_policy_local,omitempty"`

	// PodsTruncated records that the namespace holds more pods than were listed. It matters
	// because it turns "the selector matches nothing" from an incomplete answer into a
	// possibly wrong one, and that hop has to hedge rather than stay definitive.
	PodsTruncated bool `json:"pods_truncated,omitempty"`
}

ServiceChain is the input to a trace. Plain data with no client and no live objects, so Trace stays a pure function and is testable without a cluster, like every detector.

type ServicePortSpec added in v0.1.19

type ServicePortSpec struct {
	Name     string `json:"name,omitempty"`
	Port     int32  `json:"port"`
	Protocol string `json:"protocol,omitempty"`
	// TargetPort as written. A NAME here is the case worth tracing: it has to match a
	// containerPort name, and when it does not, the Service produces no endpoint port at
	// all while both the Service and the pods keep reporting healthy.
	TargetPort   string `json:"target_port"`
	TargetIsName bool   `json:"target_is_name,omitempty"`
}

ServicePortSpec is one Service port and the target it resolves to.

type ServiceView

type ServiceView struct {
	Name          string            `json:"name"`
	Selector      map[string]string `json:"selector,omitempty"`
	Ports         []string          `json:"ports,omitempty"`
	ReadyCount    int               `json:"ready_count"`
	NotReadyCount int               `json:"not_ready_count"`
	// MatchedPods is how many pods in the namespace the selector matches at all, ready or not.
	// Zero here means a label mismatch; non-zero with ReadyCount 0 means a readiness failure.
	MatchedPods int `json:"matched_pods"`
}

ServiceView plus its endpoint readiness — the pair that exposes the classic silent failure where a Service selector matches nothing and `kubectl get` looks entirely healthy.

type Severity

type Severity string

Severity is how bad the finding is if it is real. Ranked critical > warning > info.

const (
	Critical Severity = "critical"
	Warning  Severity = "warning"
	Info     Severity = "info"
)

type Snapshot

type Snapshot struct {
	Scope     string `json:"scope"`     // "workload/prod/checkout-api" or "cluster"
	Namespace string `json:"namespace"` // empty for cluster scope

	Workload    *WorkloadView    `json:"workload,omitempty"`
	ReplicaSets []ReplicaSetView `json:"replicasets,omitempty"`
	Pods        []PodView        `json:"pods,omitempty"`
	Events      []EventGroup     `json:"events,omitempty"`
	Services    []ServiceView    `json:"services,omitempty"`
	Nodes       []NodeView       `json:"nodes,omitempty"`
	HPA         *HPAView         `json:"hpa,omitempty"`
	PDB         *PDBView         `json:"pdb,omitempty"`

	// Degraded lists gather steps that failed or timed out. Detectors working from partial data
	// must dock their confidence — see Snapshot.Missing.
	Degraded []string `json:"degraded,omitempty"`

	// Notes records deliberate elisions — data we chose not to keep, not data we failed to get.
	// Kept separate from Degraded so that trimming rollout history never makes a detector think
	// the apiserver was unreachable. Silent truncation reads as "we looked at everything".
	Notes []string `json:"notes,omitempty"`
}

Snapshot is everything argus gathered about one question, projected down to an explicit allowlist of fields. Detectors are pure functions over this type.

Time is stored as *seconds ago* rather than as absolute timestamps. A fixture with absolute times silently rots: a detector asking "was this OOMKill recent?" stops firing the day after the fixture is captured, and the test still passes because no detector fires and none was expected. Relative time keeps committed fixtures meaningful forever.

func (*Snapshot) Missing

func (s *Snapshot) Missing(step string) bool

Missing reports whether a named gather step failed, so a detector can lower its confidence rather than silently reason from absent data.

type Taint added in v0.1.16

type Taint struct {
	Key    string `json:"key"`
	Value  string `json:"value,omitempty"`
	Effect string `json:"effect"`
}

Taint is a node taint reduced to what a toleration check needs.

type Toleration added in v0.1.16

type Toleration struct {
	Key      string `json:"key,omitempty"`
	Operator string `json:"operator,omitempty"` // Equal (default) | Exists
	Value    string `json:"value,omitempty"`
	Effect   string `json:"effect,omitempty"` // empty matches every effect
}

Toleration is a pod toleration, matching Kubernetes' semantics for the fields that actually decide whether a taint is tolerated.

type ToolHint

type ToolHint struct {
	Tool   string            `json:"tool"`
	Args   map[string]string `json:"args,omitempty"`
	Reason string            `json:"reason,omitempty"`
}

ToolHint points the model at the next useful call rather than making it guess.

type TraceReport added in v0.1.19

type TraceReport struct {
	Service   string `json:"service"`
	Namespace string `json:"namespace"`
	Hops      []Hop  `json:"hops"`
	// BrokenAt names the first failing hop, empty when the declared chain is intact.
	BrokenAt string `json:"broken_at,omitempty"`
	// NotChecked is load-bearing for this tool in a way it is not for the others. The
	// declared chain being intact is a common and genuinely useful result, and it means the
	// cause is in here — so an empty or vague list would send the reader back to what they
	// already ruled out.
	NotChecked []string `json:"not_checked"`
}

TraceReport is the answer: the chain, and where it gives out.

type TriageGroup added in v0.1.10

type TriageGroup struct {
	Kind      string `json:"kind"`
	Name      string `json:"name"`
	Namespace string `json:"namespace"`
	Ready     int32  `json:"ready"`
	Desired   int32  `json:"desired"`
	Pods      int    `json:"pods"`

	Findings []Finding `json:"findings"`
}

TriageGroup is one controller's findings. Triage reports controllers, never pods: forty crashlooping pods of one Deployment is one entry with a count, not forty entries.

type TriageResult added in v0.1.10

type TriageResult struct {
	Scope     string `json:"scope"`
	Scanned   int    `json:"workloads_scanned"`
	Unhealthy int    `json:"workloads_with_findings"`

	// Cluster carries findings that are about shared infrastructure rather than any one workload —
	// an unhealthy node above all. Reported once here instead of repeated on every workload it
	// happens to host, which is the same collapsing principle applied one level up.
	Cluster []Finding `json:"cluster,omitempty"`

	Groups   []TriageGroup `json:"groups"`
	Omitted  int           `json:"omitted_groups,omitempty"`
	Degraded []string      `json:"degraded,omitempty"`
	Notes    []string      `json:"notes,omitempty"`
}

TriageResult is the whole-cluster answer to "what is broken right now".

type WorkloadView

type WorkloadView struct {
	Kind      string            `json:"kind"` // Deployment | StatefulSet | DaemonSet | Rollout
	Name      string            `json:"name"`
	Namespace string            `json:"namespace"`
	Labels    map[string]string `json:"labels,omitempty"`
	Selector  map[string]string `json:"selector,omitempty"`

	Desired   int32 `json:"desired"`
	Ready     int32 `json:"ready"`
	Updated   int32 `json:"updated"`
	Available int32 `json:"available"`

	Generation         int64 `json:"generation"`
	ObservedGeneration int64 `json:"observed_generation"`
	CreatedSecondsAgo  int64 `json:"created_seconds_ago"`

	Conditions []ConditionView `json:"conditions,omitempty"`
}

WorkloadView is the controller under diagnosis.

Jump to

Keyboard shortcuts

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