diff --git a/internal/adapter/baremetal.go b/internal/adapter/baremetal.go index 98a97fe..c13093c 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,28 @@ 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) { + 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" + } + } + + 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 +} 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/collector/signals.go b/internal/collector/signals.go index 5ca624e..fb10b8e 100644 --- a/internal/collector/signals.go +++ b/internal/collector/signals.go @@ -33,10 +33,12 @@ type Signals struct { // HostInfo identifies the machine being observed. type HostInfo struct { - Hostname string `json:"hostname"` - KernelVer string `json:"kernelVersion"` - OS string `json:"os"` - Arch string `json:"arch"` + Hostname string `json:"hostname"` + KernelVer string `json:"kernelVersion"` + 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..4ec592a 100644 --- a/internal/doctor/engine.go +++ b/internal/doctor/engine.go @@ -127,14 +127,16 @@ func (e *Engine) Diagnose(ctx context.Context, signals *collector.Signals) (*Rep // Phase 3: Build report. hostname, _ := os.Hostname() report := &Report{ - Hostname: hostname, - KernelVer: signals.Host.KernelVer, - Arch: runtime.GOARCH, - StartTime: signals.Timestamp.Add(-signals.Duration), - EndTime: signals.Timestamp, - Duration: signals.Duration, - Findings: findings, - Analysis: analysis, + Hostname: hostname, + KernelVer: signals.Host.KernelVer, + Arch: runtime.GOARCH, + CloudProvider: signals.Host.CloudProvider, + InstanceType: signals.Host.InstanceType, + StartTime: signals.Timestamp.Add(-signals.Duration), + 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/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..6d4da12 100644 --- a/internal/doctor/render.go +++ b/internal/doctor/render.go @@ -147,6 +147,18 @@ 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 + switch { + case report.CloudProvider != "" && report.InstanceType != "": + cloudText = fmt.Sprintf("%s (%s)", report.CloudProvider, report.InstanceType) + case report.CloudProvider != "": + cloudText = report.CloudProvider + default: + cloudText = report.InstanceType + } + meta = append(meta, metaField(p, "Cloud", cloudText)) + } kernel := report.KernelVer if kernel != "" && report.Arch != "" { kernel += " · " + report.Arch @@ -592,16 +604,18 @@ 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"` - 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"` + 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"` + Findings []jsonFinding `json:"findings"` + Summary reportSummary `json:"summary"` + Analysis *AnalysisResponse `json:"analysis,omitempty"` + Signals any `json:"signals,omitempty"` } type jsonFinding struct { @@ -631,12 +645,14 @@ 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, - StartTime: report.StartTime, - EndTime: report.EndTime, - Duration: report.Duration.String(), + 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(), Summary: reportSummary{ Critical: crit, Warning: warn,