Skip to content
Open
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
26 changes: 26 additions & 0 deletions internal/adapter/baremetal.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"log/slog"
"os"
"strings"
)

// BareMetalAdapter enriches events with basic host metadata.
Expand Down Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions internal/cli/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 6 additions & 4 deletions internal/collector/signals.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────────────────────────
Expand Down
18 changes: 10 additions & 8 deletions internal/doctor/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions internal/doctor/finding.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 32 additions & 16 deletions internal/doctor/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
Loading