From e37ccdab9ec90f46598ef1289707ad329d57a3d9 Mon Sep 17 00:00:00 2001 From: Mayur Sahare Date: Sat, 13 Jun 2026 14:07:47 +0530 Subject: [PATCH 1/5] feat: Add Cloud Provider and Instance Type Enrichment for VM deployments Signed-off-by: Mayur Sahare --- internal/adapter/baremetal.go | 25 +++++++++++++++++++++++++ internal/collector/signals.go | 6 ++++-- internal/doctor/engine.go | 12 ++++++++---- internal/doctor/finding.go | 8 +++++--- internal/doctor/render.go | 27 +++++++++++++++++++++------ 5 files changed, 63 insertions(+), 15 deletions(-) diff --git a/internal/adapter/baremetal.go b/internal/adapter/baremetal.go index 98a97fe..b40e023 100644 --- a/internal/adapter/baremetal.go +++ b/internal/adapter/baremetal.go @@ -7,6 +7,7 @@ import ( "context" "log/slog" "os" + "strings" ) // BareMetalAdapter enriches events with basic host metadata. @@ -41,3 +42,27 @@ func (a *BareMetalAdapter) Enrich(meta *EventMeta) { meta.CgroupPath = cgroupPathForPID(meta.PID) } } + +// DetectCloud tries to determine the cloud provider and instance type +// by reading DMI sysfs files. +func DetectCloud() (provider, instanceType string) { + vendorBytes, _ := os.ReadFile("/sys/class/dmi/id/sys_vendor") + vendor := strings.TrimSpace(string(vendorBytes)) + switch { + case strings.Contains(vendor, "Amazon EC2"): + provider = "AWS EC2" + case strings.Contains(vendor, "Google"): + provider = "Google Cloud" + case strings.Contains(vendor, "Microsoft Corporation"): + provider = "Azure" + } + + productBytes, _ := os.ReadFile("/sys/class/dmi/id/product_name") + product := strings.TrimSpace(string(productBytes)) + if product != "" { + instanceType = product + } + + return provider, instanceType +} + diff --git a/internal/collector/signals.go b/internal/collector/signals.go index 5ca624e..340bb6d 100644 --- a/internal/collector/signals.go +++ b/internal/collector/signals.go @@ -35,8 +35,10 @@ type Signals struct { type HostInfo struct { Hostname string `json:"hostname"` KernelVer string `json:"kernelVersion"` - OS string `json:"os"` - Arch string `json:"arch"` + OS string `json:"os"` + Arch string `json:"arch"` + CloudProvider string `json:"cloudProvider,omitempty"` + InstanceType string `json:"instanceType,omitempty"` } // ─── Percentiles ──────────────────────────────────────────────────────────── diff --git a/internal/doctor/engine.go b/internal/doctor/engine.go index 69dd1dd..e020ac7 100644 --- a/internal/doctor/engine.go +++ b/internal/doctor/engine.go @@ -10,6 +10,7 @@ import ( "runtime" "time" + "github.com/optiqor/kerno/internal/adapter" "github.com/optiqor/kerno/internal/collector" "github.com/optiqor/kerno/internal/config" ) @@ -126,11 +127,14 @@ func (e *Engine) Diagnose(ctx context.Context, signals *collector.Signals) (*Rep // Phase 3: Build report. hostname, _ := os.Hostname() + provider, instanceType := adapter.DetectCloud() report := &Report{ - Hostname: hostname, - KernelVer: signals.Host.KernelVer, - Arch: runtime.GOARCH, - StartTime: signals.Timestamp.Add(-signals.Duration), + Hostname: hostname, + KernelVer: signals.Host.KernelVer, + Arch: runtime.GOARCH, + CloudProvider: provider, + InstanceType: instanceType, + StartTime: signals.Timestamp.Add(-signals.Duration), EndTime: signals.Timestamp, Duration: signals.Duration, Findings: findings, diff --git a/internal/doctor/finding.go b/internal/doctor/finding.go index df10832..183b356 100644 --- a/internal/doctor/finding.go +++ b/internal/doctor/finding.go @@ -116,9 +116,11 @@ func (f *Finding) ETAString() string { // Report is the complete output of a doctor diagnostic run. type Report struct { // Host identifies the machine analyzed. - Hostname string - KernelVer string - Arch string + Hostname string + KernelVer string + Arch string + CloudProvider string + InstanceType string // Timing records the analysis window. StartTime time.Time diff --git a/internal/doctor/render.go b/internal/doctor/render.go index 5dcc394..4d4472b 100644 --- a/internal/doctor/render.go +++ b/internal/doctor/render.go @@ -147,6 +147,17 @@ func (r *PrettyRenderer) renderHeader(w io.Writer, report *Report, p palette) { if report.Environment != "" { meta = append(meta, metaField(p, "Env", report.Environment)) } + if report.CloudProvider != "" || report.InstanceType != "" { + var cloudText string + if report.CloudProvider != "" && report.InstanceType != "" { + cloudText = fmt.Sprintf("%s (%s)", report.CloudProvider, report.InstanceType) + } else if report.CloudProvider != "" { + cloudText = report.CloudProvider + } else { + cloudText = report.InstanceType + } + meta = append(meta, metaField(p, "Cloud", cloudText)) + } kernel := report.KernelVer if kernel != "" && report.Arch != "" { kernel += " · " + report.Arch @@ -592,9 +603,11 @@ type JSONRenderer struct { // jsonReport is the JSON-serializable report structure. type jsonReport struct { - Hostname string `json:"hostname"` - KernelVer string `json:"kernelVersion"` - Arch string `json:"arch"` + Hostname string `json:"hostname"` + KernelVer string `json:"kernelVersion"` + Arch string `json:"arch"` + CloudProvider string `json:"cloudProvider,omitempty"` + InstanceType string `json:"instanceType,omitempty"` StartTime time.Time `json:"startTime"` EndTime time.Time `json:"endTime"` Duration string `json:"duration"` @@ -631,9 +644,11 @@ func (r *JSONRenderer) Render(w io.Writer, report *Report) error { crit, warn, info := report.CountBySeverity() jr := jsonReport{ - Hostname: report.Hostname, - KernelVer: report.KernelVer, - Arch: report.Arch, + Hostname: report.Hostname, + KernelVer: report.KernelVer, + Arch: report.Arch, + CloudProvider: report.CloudProvider, + InstanceType: report.InstanceType, StartTime: report.StartTime, EndTime: report.EndTime, Duration: report.Duration.String(), From 12a33b9e0542fcdb3ecb250fa776eafe5594bc66 Mon Sep 17 00:00:00 2001 From: Mayur Sahare Date: Sat, 13 Jun 2026 14:15:48 +0530 Subject: [PATCH 2/5] fix: Handle errors from os.ReadFile in DetectCloud to satisfy linter Signed-off-by: Mayur Sahare --- internal/adapter/baremetal.go | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/internal/adapter/baremetal.go b/internal/adapter/baremetal.go index b40e023..d33a42e 100644 --- a/internal/adapter/baremetal.go +++ b/internal/adapter/baremetal.go @@ -46,21 +46,23 @@ func (a *BareMetalAdapter) Enrich(meta *EventMeta) { // DetectCloud tries to determine the cloud provider and instance type // by reading DMI sysfs files. func DetectCloud() (provider, instanceType string) { - vendorBytes, _ := os.ReadFile("/sys/class/dmi/id/sys_vendor") - vendor := strings.TrimSpace(string(vendorBytes)) - switch { - case strings.Contains(vendor, "Amazon EC2"): - provider = "AWS EC2" - case strings.Contains(vendor, "Google"): - provider = "Google Cloud" - case strings.Contains(vendor, "Microsoft Corporation"): - provider = "Azure" + if vendorBytes, err := os.ReadFile("/sys/class/dmi/id/sys_vendor"); err == nil { + vendor := strings.TrimSpace(string(vendorBytes)) + switch { + case strings.Contains(vendor, "Amazon EC2"): + provider = "AWS EC2" + case strings.Contains(vendor, "Google"): + provider = "Google Cloud" + case strings.Contains(vendor, "Microsoft Corporation"): + provider = "Azure" + } } - productBytes, _ := os.ReadFile("/sys/class/dmi/id/product_name") - product := strings.TrimSpace(string(productBytes)) - if product != "" { - instanceType = product + if productBytes, err := os.ReadFile("/sys/class/dmi/id/product_name"); err == nil { + product := strings.TrimSpace(string(productBytes)) + if product != "" { + instanceType = product + } } return provider, instanceType From a54ead48f8e52a8bd6c374bc4ad1cca442f373ad Mon Sep 17 00:00:00 2001 From: Mayur Sahare Date: Sat, 13 Jun 2026 14:21:58 +0530 Subject: [PATCH 3/5] refactor: Move CloudProvider detection to cli to fix dependency lint Signed-off-by: Mayur Sahare --- internal/cli/doctor.go | 4 ++++ internal/doctor/engine.go | 6 ++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index 5076c11..d6e05f0 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -528,6 +528,10 @@ func runDiagnosticCycle( // Phase 2: Gather combined signal snapshot from all collectors. signals := registry.Signals(opts.duration) + provider, instanceType := adapter.DetectCloud() + signals.Host.CloudProvider = provider + signals.Host.InstanceType = instanceType + // Phase 3: Run diagnostic engine (rules + optional AI). report, err := engine.Diagnose(ctx, signals) if err != nil { diff --git a/internal/doctor/engine.go b/internal/doctor/engine.go index e020ac7..0038421 100644 --- a/internal/doctor/engine.go +++ b/internal/doctor/engine.go @@ -10,7 +10,6 @@ import ( "runtime" "time" - "github.com/optiqor/kerno/internal/adapter" "github.com/optiqor/kerno/internal/collector" "github.com/optiqor/kerno/internal/config" ) @@ -127,13 +126,12 @@ func (e *Engine) Diagnose(ctx context.Context, signals *collector.Signals) (*Rep // Phase 3: Build report. hostname, _ := os.Hostname() - provider, instanceType := adapter.DetectCloud() report := &Report{ Hostname: hostname, KernelVer: signals.Host.KernelVer, Arch: runtime.GOARCH, - CloudProvider: provider, - InstanceType: instanceType, + CloudProvider: signals.Host.CloudProvider, + InstanceType: signals.Host.InstanceType, StartTime: signals.Timestamp.Add(-signals.Duration), EndTime: signals.Timestamp, Duration: signals.Duration, From 5ca050d4b5e8287b55863b704cee977b8d16750b Mon Sep 17 00:00:00 2001 From: Mayur Sahare Date: Sat, 13 Jun 2026 14:31:47 +0530 Subject: [PATCH 4/5] style: run gofmt to fix spacing and alignment in structs Signed-off-by: Mayur Sahare --- internal/adapter/baremetal.go | 1 - internal/collector/signals.go | 4 ++-- internal/doctor/engine.go | 8 ++++---- internal/doctor/render.go | 20 ++++++++++---------- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/internal/adapter/baremetal.go b/internal/adapter/baremetal.go index d33a42e..c13093c 100644 --- a/internal/adapter/baremetal.go +++ b/internal/adapter/baremetal.go @@ -67,4 +67,3 @@ func DetectCloud() (provider, instanceType string) { return provider, instanceType } - diff --git a/internal/collector/signals.go b/internal/collector/signals.go index 340bb6d..fb10b8e 100644 --- a/internal/collector/signals.go +++ b/internal/collector/signals.go @@ -33,8 +33,8 @@ type Signals struct { // HostInfo identifies the machine being observed. type HostInfo struct { - Hostname string `json:"hostname"` - KernelVer string `json:"kernelVersion"` + Hostname string `json:"hostname"` + KernelVer string `json:"kernelVersion"` OS string `json:"os"` Arch string `json:"arch"` CloudProvider string `json:"cloudProvider,omitempty"` diff --git a/internal/doctor/engine.go b/internal/doctor/engine.go index 0038421..4ec592a 100644 --- a/internal/doctor/engine.go +++ b/internal/doctor/engine.go @@ -133,10 +133,10 @@ func (e *Engine) Diagnose(ctx context.Context, signals *collector.Signals) (*Rep CloudProvider: signals.Host.CloudProvider, InstanceType: signals.Host.InstanceType, StartTime: signals.Timestamp.Add(-signals.Duration), - EndTime: signals.Timestamp, - Duration: signals.Duration, - Findings: findings, - Analysis: analysis, + EndTime: signals.Timestamp, + Duration: signals.Duration, + Findings: findings, + Analysis: analysis, // Carry the raw signals through so the JSON renderer can // surface them for debugging — the pretty renderer ignores it. Signals: signals, diff --git a/internal/doctor/render.go b/internal/doctor/render.go index 4d4472b..ec51d5a 100644 --- a/internal/doctor/render.go +++ b/internal/doctor/render.go @@ -608,13 +608,13 @@ type jsonReport struct { Arch string `json:"arch"` CloudProvider string `json:"cloudProvider,omitempty"` InstanceType string `json:"instanceType,omitempty"` - StartTime time.Time `json:"startTime"` - EndTime time.Time `json:"endTime"` - Duration string `json:"duration"` - Findings []jsonFinding `json:"findings"` - Summary reportSummary `json:"summary"` - Analysis *AnalysisResponse `json:"analysis,omitempty"` - Signals any `json:"signals,omitempty"` + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Duration string `json:"duration"` + Findings []jsonFinding `json:"findings"` + Summary reportSummary `json:"summary"` + Analysis *AnalysisResponse `json:"analysis,omitempty"` + Signals any `json:"signals,omitempty"` } type jsonFinding struct { @@ -649,9 +649,9 @@ func (r *JSONRenderer) Render(w io.Writer, report *Report) error { Arch: report.Arch, CloudProvider: report.CloudProvider, InstanceType: report.InstanceType, - StartTime: report.StartTime, - EndTime: report.EndTime, - Duration: report.Duration.String(), + StartTime: report.StartTime, + EndTime: report.EndTime, + Duration: report.Duration.String(), Summary: reportSummary{ Critical: crit, Warning: warn, From 74d1d90465b46cc7daa38594d1ab8352237f1606 Mon Sep 17 00:00:00 2001 From: Mayur Sahare Date: Sat, 13 Jun 2026 14:42:50 +0530 Subject: [PATCH 5/5] fix: rewrite ifElseChain to switch in render.go to satisfy gocritic Signed-off-by: Mayur Sahare --- internal/doctor/render.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/doctor/render.go b/internal/doctor/render.go index ec51d5a..6d4da12 100644 --- a/internal/doctor/render.go +++ b/internal/doctor/render.go @@ -149,11 +149,12 @@ func (r *PrettyRenderer) renderHeader(w io.Writer, report *Report, p palette) { } if report.CloudProvider != "" || report.InstanceType != "" { var cloudText string - if report.CloudProvider != "" && report.InstanceType != "" { + switch { + case report.CloudProvider != "" && report.InstanceType != "": cloudText = fmt.Sprintf("%s (%s)", report.CloudProvider, report.InstanceType) - } else if report.CloudProvider != "" { + case report.CloudProvider != "": cloudText = report.CloudProvider - } else { + default: cloudText = report.InstanceType } meta = append(meta, metaField(p, "Cloud", cloudText))