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
6 changes: 3 additions & 3 deletions deploy/chart/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ apiVersion: v2
name: nodevitals
description: Unified hardware telemetry agent for Kubernetes nodes
type: application
version: 0.9.2
appVersion: "0.9.2"
version: 0.9.3
appVersion: "0.9.3"
kubeVersion: ">=1.26.0-0"

home: https://github.com/keiailab/nodevitals
Expand Down Expand Up @@ -47,4 +47,4 @@ annotations:
url: https://raw.githubusercontent.com/KeiaiLab/nodevitals/main/docs/branding/symbol.png
artifacthub.io/images: |
- name: nodevitals
image: ghcr.io/keiailab/nodevitals:0.9.2
image: ghcr.io/keiailab/nodevitals:0.9.3
10 changes: 8 additions & 2 deletions deploy/chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,14 @@ updateStrategy:
nodeExporter:
enabled: false
# nativeCollectors 가 true 면 nodevitals 의 자체 Go 수집기(internal/nodecompat)가
# /proc 기반 node_* 지표(loadavg, filefd, entropy, procs, vmstat, uname, osrelease)를
# 직접 방출한다.
# /proc 기반 node_* 지표(loadavg, filefd, entropy, vmstat, uname, osrelease)를
# 직접 방출하고, 그것들이 대체하는 upstream collector 는 자동으로 꺼진다.
#
# 대체는 collector 단위라 전부-아니면-전무다: 자체 수집기가 upstream collector 의
# 메트릭 일부만 내면 나머지는 통째로 사라진다(2026-08-12 카나리 실측 — procs 가
# node_procs_running/blocked 만 내면서 stat 을 끄는 바람에 boot_time·
# context_switches·forks·intr 넷이 소실됐다). parity 테스트가 두 집합이 정확히
# 일치하는지 CI 에서 강제한다.
nativeCollectors: true
# filesystem collector 는 호스트의 모든 마운트를 statfs 해야 하므로 호스트 루트를
# 읽기전용으로 마운트한다(upstream node_exporter 차트와 동일). 이는 컨테이너에
Expand Down
2 changes: 1 addition & 1 deletion docs/COMPATIBILITY.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# nodevitals — 서비스 전수 호환성 및 연동 명세서 (Compatibility Matrix)

> 저장소: [`github.com/KeiaiLab/nodevitals`](https://github.com/KeiaiLab/nodevitals)
> 기준 버전: `v0.9.2` (Chart v0.9.2)
> 기준 버전: `v0.9.3` (Chart v0.9.3)
> 최종 검증 일시: 2026년 8월 12일

본 문서는 `nodevitals`가 연동되는 주요 인프라 서비스, 관측 플랫폼, GPU 오퍼레이터, 가상머신(VM) 환경 간의 명시적 호환성 계약(Compatibility Contract)과 실측 검증 결과를 제공합니다.
Expand Down
79 changes: 0 additions & 79 deletions internal/nodecompat/collision_linux_test.go

This file was deleted.

6 changes: 5 additions & 1 deletion internal/nodecompat/nodecompat.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ func New(procRoot, sysRoot, rootFS string, log *slog.Logger) *Exporter {
newLoadAvg(procRoot),
newFileFD(procRoot),
newEntropy(procRoot),
newProcs(procRoot),
newVMStat(procRoot),
// procs(node_procs_running/blocked)는 의도적으로 없다. upstream 의
// "stat" collector 가 같은 /proc/stat 에서 그 둘에 더해
// boot_time_seconds·context_switches_total·forks_total·intr_total 까지
// 내므로, 둘만 내면서 stat 을 끄면 나머지 넷이 통째로 사라진다
// (2026-08-12 카나리 실측). parity 테스트가 이 조건을 강제한다.
newUname(),
newOSRelease(rootFS),
},
Expand Down
114 changes: 114 additions & 0 deletions internal/nodecompat/parity_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//go:build linux

// 이 검사는 Linux 에서만 의미가 있다. entropy·filefd·stat·vmstat 은 upstream 에
// Linux 전용 구현뿐이라, darwin 에서는 비교 대상 자체가 비어 통과해버린다.
package nodecompat_test

import (
"io"
"log/slog"
"regexp"
"testing"

"github.com/prometheus/client_golang/prometheus"
necollector "github.com/prometheus/node_exporter/collector"

"github.com/KeiaiLab/nodevitals/internal/nodecompat"
"github.com/KeiaiLab/nodevitals/internal/nodeexporter"
)

var fqName = regexp.MustCompile(`fqName: "([^"]+)"`)

// 자체 수집기가 어떤 upstream collector 를 대체한다고 선언하면 그 collector 는
// 통째로 꺼진다. 따라서 두 집합이 정확히 같아야 한다.
//
// upstream 에만 있는 이름 → 그 메트릭은 아무데서도 나오지 않는다(손실)
// native 에만 있는 이름 → 끄지 못한 다른 collector 와 충돌한다(중복)
//
// 손실 쪽이 특히 잡기 어렵다. 이름만 보고는 어느 collector 소관인지 알 수 없기
// 때문이다 — upstream 의 "stat" 은 node_procs_running 뿐 아니라
// node_boot_time_seconds·node_context_switches_total·node_forks_total·
// node_intr_total 도 낸다. 실측으로 대조하지 않으면 넷이 조용히 사라진다.
func TestNativeCollectorsMatchTheUpstreamCollectorsTheyReplace(t *testing.T) {
quiet := slog.New(slog.NewTextHandler(io.Discard, nil))

// 차단 플래그 없이 만든다 — 대체 대상 collector 를 개별로 수집해야 하므로.
// node_exporter 의 collector 들은 init() 에서 전역 kingpin 에 플래그를 등록하고
// nodeexporter.New 가 그것을 딱 한 번 파싱하므로, 이 테스트가 이 패키지의
// 유일한 호출자여야 한다.
c, err := nodeexporter.New(nodeexporter.Config{ProcPath: "/proc", SysPath: "/sys"}, quiet)
if err != nil {
t.Fatalf("build embedded node_exporter: %v", err)
}
node, ok := c.(*necollector.NodeCollector)
if !ok {
t.Fatalf("embedded collector is %T, not a *NodeCollector", c)
}

upstream := map[string]bool{}
for _, name := range nodecompat.SupersededCollectors() {
sub, ok := node.Collectors[name]
if !ok {
t.Errorf("nodecompat claims to supersede upstream collector %q, which is not enabled "+
"(a typo here means the upstream collector keeps running and duplicates the native one)", name)
continue
}
for n := range namesFromUpdate(t, sub) {
upstream[n] = true
}
}
if len(upstream) == 0 {
t.Fatal("superseded upstream collectors emitted nothing; this comparison would pass vacuously")
}

native := namesFromCollector(t, nodecompat.New("/proc", "/sys", "/", quiet))

for n := range upstream {
if !native[n] {
t.Errorf("%q is emitted by a superseded upstream collector but not by the native one; "+
"disabling that collector deletes the metric outright", n)
}
}
for n := range native {
if !upstream[n] {
t.Errorf("%q is emitted natively but by none of the superseded upstream collectors; "+
"whichever upstream collector owns it is still enabled and will collide", n)
}
}
}

// namesFromUpdate collects one embedded collector on its own, which is the only
// way to learn which metrics that particular collector owns.
func namesFromUpdate(t *testing.T, c necollector.Collector) map[string]bool {
t.Helper()
ch := make(chan prometheus.Metric, 4096)
if err := c.Update(ch); err != nil {
// 컨테이너에 없는 하드웨어를 읽는 collector 는 정상적으로 실패한다.
t.Logf("collector update reported (may be expected): %v", err)
}
close(ch)
out := map[string]bool{}
for m := range ch {
if g := fqName.FindStringSubmatch(m.Desc().String()); g != nil {
out[g[1]] = true
}
}
return out
}

func namesFromCollector(t *testing.T, c prometheus.Collector) map[string]bool {
t.Helper()
reg := prometheus.NewRegistry()
if err := reg.Register(c); err != nil {
t.Fatalf("register collector: %v", err)
}
families, err := reg.Gather()
if err != nil {
t.Logf("gather reported: %v", err)
}
out := map[string]bool{}
for _, f := range families {
out[f.GetName()] = true
}
return out
}
70 changes: 0 additions & 70 deletions internal/nodecompat/procs.go

This file was deleted.

31 changes: 0 additions & 31 deletions internal/nodecompat/procs_test.go

This file was deleted.

1 change: 0 additions & 1 deletion internal/nodecompat/superseded_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ func TestNoCollectorFlagsCoverEverySupersededCollector(t *testing.T) {
func TestSupersededNamesAreUpstreamNamesNotLocalNames(t *testing.T) {
e := New("/proc", "/sys", "/", nil)
want := map[string]string{
"procs": "stat",
"osrelease": "os",
}
for _, sub := range e.subs {
Expand Down
Loading