Overview

Three ops engineers, 200 servers. Every morning: SSH into each one, check disk, memory, CPU, connection count, certificate expiry… the entire morning is gone. When a sudden outage hits, there is no time to inspect — the problem has already exploded in user complaints.

This is not an isolated case. Many small-to-mid teams still do ops inspection the “manual + script” way — a few Shell scripts scattered across machines, unmaintained, nobody knows when they last ran, and nobody reads the output.

An automated inspection platform solves exactly this problem. It is not just “centralizing scripts to run together” — it is a complete system: plugin-based check items, a concurrent execution engine, flexible alerting strategies, and readable inspection reports. This article covers everything from architecture design to code implementation, showing how to build a production-ready inspection platform in Go.

Why Go instead of Python? Three reasons: single-binary deployment, goroutine concurrency model naturally suited for inspection workloads, and cross-compilation makes distribution to different server architectures trivial. Python can do it too, but the pain of distributing Python environments and dependencies across 200 machines is something anyone who has tried it understands.

What Problem Does an Inspection Platform Solve

Pain Points of Traditional Inspection

Let us look at how painful “manual inspection” really is:

Pain PointSymptomImpact
Incomplete coverageOnly 50 of 200 machines get checked regularly; the rest “have never had issues”Hidden risks accumulate, exploding when least expected
No standardsEngineer A uses df -h, B uses df -hT, C wrote a Python script only they understandChanging personnel means starting from scratch; results cannot be compared across machines
No historyInspection results live in personal notes, taken when someone leavesCannot trace trends — “it was fine last week, how did it suddenly fill up?”
Delayed alertsInspection finds disk at 95%, but nobody sees it in timeHours or days between discovery and action
Wasted manpower3 people spend 2-3 hours daily on inspection, ~200 person-hours per monthEquivalent to 1.25 FTE doing nothing but manual checks

Core Capabilities of a Platform

A qualified inspection platform needs these capabilities:

┌─────────────────────────────────────────────────────┐
│              Inspection Platform Core Capabilities    │
├──────────┬──────────┬──────────┬──────────┬─────────┤
│  Check    │ Scheduling│  Alert   │  Report  │  Asset  │
│  Engine   │  System   │  Engine  │ Generator │ Management│
│          │          │          │          │         │
│ Plugin   │ Cron      │ Multi-   │ HTML     │ Host    │
│ based    │ trigger   │ level    │ reports  │ inventory│
│ Concurrent│ Manual   │ thresholds│ Trend   │ Group   │
│ execution│ trigger   │ Dedup    │ charts   │ management│
│ Timeout  │ Catch-up  │ Channels │ Diff     │ Tag     │
│ control  │ execution │          │ comparison│ system  │
└──────────┴──────────┴──────────┴──────────┴─────────┘

In short: configure check items and thresholds, the platform runs automatically at scheduled times, alerts immediately on issues, and archives every result for traceability.

Architecture Design

Overall Architecture

┌──────────────────────────────────────────────────────┐
│                    API / Web UI                       │
│         (Config management, report viewing, manual)   │
├──────────────────────────────────────────────────────┤
│                                                       │
│  ┌─────────────┐  ┌──────────────┐  ┌─────────────┐ │
│  │  Scheduler   │  │  Executor    │  │  Alert      │ │
│  │             │  │              │  │  Engine     │ │
│  │ Cron trigger│──>│ Worker Pool  │──>│ Threshold  │ │
│  │ Manual      │  │ Concurrent   │  │ judgment   │ │
│  │ Catch-up    │  │ Timeout ctrl │  │ Dedup      │ │
│  └─────────────┘  └──────────────┘  │ Multi-channel│ │
│         │                │          └─────────────┘ │
│         │         ┌──────┴──────┐        │          │
│         │         │  Plugin     │        │          │
│         │         │  Registry   │        │          │
│         │         │             │        │          │
│         │         │ disk_check  │        │          │
│         │         │ cpu_check   │        │          │
│         │         │ cert_check  │        │          │
│         │         │ conn_check  │        │          │
│         │         │ ...         │        │          │
│         │         └─────────────┘        │          │
│         │                │                │          │
│  ┌──────┴────────────────┴────────────────┴──────┐  │
│  │                  Storage Layer                 │  │
│  │  SQLite/PostgreSQL (results, config, assets)   │  │
│  └───────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────┘

Layer Responsibilities

LayerResponsibilityTech Choice
AccessConfig management, report display, manual triggersHTTP API + simple Web UI
SchedulingTimed inspection triggers, task queue managementGo cron library + channel
ExecutionConcurrent plugin execution, timeout controlgoroutine + Worker Pool
PluginSpecific check logic (disk, CPU, cert, etc.)Go interface plugin system
AlertThreshold evaluation, alert dedup, notificationwebhook + email + IM
StorageResult, config, asset persistenceSQLite (single) or PostgreSQL (cluster)

Why Plugin-Based

Check items change constantly. Today you check disk, tomorrow you add certificate expiry, the day after you need database connection count. If check logic is hardcoded in the main program, every new check item requires modifying the main program, recompiling, and redeploying.

The plugin approach: adding a check item means writing a plugin file, registering it with the plugin manager, and restarting the platform. No changes to existing logic or the main program.

Go plugin implementation options:

ApproachProsConsUse Case
Go plugin packageNative, high performanceLinux/macOS only, build version must matchPure Go, controlled platform
interface + registrySimple, reliable, cross-platformRequires recompiling main programSmall-to-mid scale, infrequent changes
gRPC pluginsLanguage-agnostic, process isolationComplex, RPC overheadMulti-language, strong isolation
External script executionMost flexible, any languagePoor performance, external depsRapid prototyping, legacy script reuse

Considering cross-platform needs and deployment simplicity, I chose the interface + registry approach. Although adding plugins requires recompilation, Go compiles fast, and a config file controls which plugins are enabled — flexible enough.

Core Code Implementation

Project Structure

sre-inspector/
├── cmd/
   └── inspector/
       └── main.go           # Entry point
├── internal/
   ├── config/
      └── config.go         # Config loading
   ├── scheduler/
      └── scheduler.go      # Scheduling engine
   ├── executor/
      └── executor.go       # Execution engine
   ├── checker/
      ├── checker.go        # Checker interface definition
      ├── registry.go       # Plugin registry
      ├── disk.go           # Disk check plugin
      ├── cpu.go            # CPU check plugin
      ├── memory.go         # Memory check plugin
      ├── certificate.go    # Certificate expiry check plugin
      ├── connection.go     # Connection count check plugin
      └── process.go        # Process check plugin
   ├── alert/
      └── alert.go          # Alert engine
   ├── report/
      └── report.go         # Report generation
   └── storage/
       └── storage.go        # Data storage
├── configs/
   └── inspector.yaml        # Configuration
├── go.mod
└── go.sum

Checker Interface Definition

The core of plugin architecture is a well-defined interface. Every check item implements it:

// internal/checker/checker.go

package checker

import (
    "context"
    "time"
)

// CheckResult result of a single check
type CheckResult struct {
    CheckerName  string             // Check item name
    Target       string             // Check target (IP, hostname, etc.)
    Status       Status             // Status: OK/Warning/Critical
    Message      string             // Result description
    Metrics      map[string]float64 // Metric data (disk usage, CPU load, etc.)
    CheckedAt    time.Time          // Check time
    Duration     time.Duration      // Check duration
}

// Status check status
type Status int

const (
    StatusOK      Status = 0 // Normal
    StatusWarning Status = 1 // Warning
    StatusCritical Status = 2 // Critical
    StatusUnknown Status = 3 // Unknown (check failed)
)

func (s Status) String() string {
    switch s {
    case StatusOK:
        return "OK"
    case StatusWarning:
        return "WARNING"
    case StatusCritical:
        return "CRITICAL"
    default:
        return "UNKNOWN"
    }
}

// Checker interface that all plugins must implement
type Checker interface {
    // Name returns the checker name
    Name() string

    // Description returns the checker description
    Description() string

    // Check executes the check
    // ctx for timeout control and cancellation
    // target is the check target (host IP, hostname, etc.)
    // params are check parameters (thresholds, etc.)
    Check(ctx context.Context, target string, params map[string]string) CheckResult
}

Plugin Registry

// internal/checker/registry.go

package checker

import (
    "fmt"
    "sync"
)

// Registry plugin registry
type Registry struct {
    mu       sync.RWMutex
    checkers map[string]Checker
}

// NewRegistry creates a registry instance
func NewRegistry() *Registry {
    return &Registry{
        checkers: make(map[string]Checker),
    }
}

// Register registers a checker
func (r *Registry) Register(c Checker) error {
    r.mu.Lock()
    defer r.mu.Unlock()

    name := c.Name()
    if _, exists := r.checkers[name]; exists {
        return fmt.Errorf("checker %q already registered", name)
    }
    r.checkers[name] = c
    return nil
}

// Get retrieves a checker by name
func (r *Registry) Get(name string) (Checker, bool) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    c, ok := r.checkers[name]
    return c, ok
}

// List returns all registered checker names
func (r *Registry) List() []string {
    r.mu.RLock()
    defer r.mu.RUnlock()
    names := make([]string, 0, len(r.checkers))
    for name := range r.checkers {
        names = append(names, name)
    }
    return names
}

// RegisterBuiltin registers built-in checkers
func (r *Registry) RegisterBuiltin() {
    r.Register(&DiskChecker{})
    r.Register(&CPUChecker{})
    r.Register(&MemoryChecker{})
    r.Register(&CertificateChecker{})
    r.Register(&ConnectionChecker{})
    r.Register(&ProcessChecker{})
}

Disk Check Plugin Implementation

Using disk check as an example, here is a complete plugin:

// internal/checker/disk.go

package checker

import (
    "context"
    "fmt"
    "os/exec"
    "strconv"
    "strings"
    "time"
)

// DiskChecker disk usage checker
type DiskChecker struct{}

func (d *DiskChecker) Name() string        { return "disk" }
func (d *DiskChecker) Description() string  { return "Check disk partition usage" }

func (d *DiskChecker) Check(ctx context.Context, target string, params map[string]string) CheckResult {
    start := time.Now()

    // Parse threshold params, default warning 80% critical 95%
    warnThreshold := parseFloatParam(params, "warning", 80.0)
    critThreshold := parseFloatParam(params, "critical", 95.0)

    // Execute remote command via SSH (if target is not localhost)
    cmd := exec.CommandContext(ctx, "ssh", "-o", "ConnectTimeout=5", target, "df -h --output=pcent,target -x tmpfs -x devtmpfs")
    output, err := cmd.CombinedOutput()
    if err != nil {
        return CheckResult{
            CheckerName: d.Name(),
            Target:      target,
            Status:      StatusUnknown,
            Message:      fmt.Sprintf("SSH execution failed: %v, output: %s", err, string(output)),
            CheckedAt:    start,
            Duration:     time.Since(start),
        }
    }

    // Parse df output, find the partition with highest usage
    var maxUsage float64
    var maxPartitions []string
    lines := strings.Split(strings.TrimSpace(string(output)), "\n")
    for i, line := range lines {
        if i == 0 {
            continue // skip header
        }
        fields := strings.Fields(line)
        if len(fields) < 2 {
            continue
        }
        usageStr := strings.TrimSuffix(fields[0], "%")
        usage, err := strconv.ParseFloat(usageStr, 64)
        if err != nil {
            continue
        }
        if usage > maxUsage {
            maxUsage = usage
            maxPartitions = append(maxPartitions[:0], fields[1])
        } else if usage == maxUsage {
            maxPartitions = append(maxPartitions, fields[1])
        }
    }

    // Determine status based on thresholds
    status := StatusOK
    message := fmt.Sprintf("Max disk usage: %.1f%% (%s)", maxUsage, strings.Join(maxPartitions, ", "))
    if maxUsage >= critThreshold {
        status = StatusCritical
        message = fmt.Sprintf("Disk usage critical: %.1f%% (%s) >= %.0f%%", maxUsage, strings.Join(maxPartitions, ", "), critThreshold)
    } else if maxUsage >= warnThreshold {
        status = StatusWarning
        message = fmt.Sprintf("Disk usage warning: %.1f%% (%s) >= %.0f%%", maxUsage, strings.Join(maxPartitions, ", "), warnThreshold)
    }

    return CheckResult{
        CheckerName: d.Name(),
        Target:      target,
        Status:      status,
        Message:      message,
        Metrics:      map[string]float64{"disk_usage_percent": maxUsage},
        CheckedAt:    start,
        Duration:     time.Since(start),
    }
}

func parseFloatParam(params map[string]string, key string, defaultVal float64) float64 {
    if val, ok := params[key]; ok {
        if f, err := strconv.ParseFloat(val, 64); err == nil {
            return f
        }
    }
    return defaultVal
}

Execution Engine: Concurrency Control

Inspecting 200 machines serially takes half an hour. Concurrent execution is essential, but cannot be unlimited — too many SSH connections will overwhelm the target machines’ sshd and exhaust local file descriptors.

// internal/executor/executor.go

package executor

import (
    "context"
    "fmt"
    "log"
    "sync"
    "time"

    "sre-inspector/internal/checker"
)

// Executor execution engine
type Executor struct {
    registry    *checker.Registry
    maxWorkers  int           // max concurrency
    timeout     time.Duration // per-check timeout
}

// NewExecutor creates an execution engine
func NewExecutor(registry *checker.Registry, maxWorkers int, timeout time.Duration) *Executor {
    return &Executor{
        registry:   registry,
        maxWorkers: maxWorkers,
        timeout:    timeout,
    }
}

// Task a single inspection task
type Task struct {
    CheckerName string            // checker name
    Target      string            // target host
    Params      map[string]string // check parameters
}

// RunBatch executes inspection tasks in batch
func (e *Executor) RunBatch(tasks []Task) []checker.CheckResult {
    // Buffered channel as task queue
    taskCh := make(chan Task, len(tasks))
    resultCh := make(chan checker.CheckResult, len(tasks))

    // Fill the task queue
    for _, task := range tasks {
        taskCh <- task
    }
    close(taskCh)

    // Start worker pool
    var wg sync.WaitGroup
    for i := 0; i < e.maxWorkers; i++ {
        wg.Add(1)
        go e.worker(&wg, taskCh, resultCh)
    }

    // Wait for all workers to finish
    go func() {
        wg.Wait()
        close(resultCh)
    }()

    // Collect results
    results := make([]checker.CheckResult, 0, len(tasks))
    for result := range resultCh {
        results = append(results, result)
    }
    return results
}

// worker goroutine
func (e *Executor) worker(wg *sync.WaitGroup, taskCh <-chan Task, resultCh chan<- checker.CheckResult) {
    defer wg.Done()
    for task := range taskCh {
        result := e.runSingle(task)
        resultCh <- result
    }
}

// runSingle executes a single check task
func (e *Executor) runSingle(task Task) checker.CheckResult {
    c, ok := e.registry.Get(task.CheckerName)
    if !ok {
        return checker.CheckResult{
            CheckerName: task.CheckerName,
            Target:      task.Target,
            Status:      checker.StatusUnknown,
            Message:      fmt.Sprintf("checker %q not registered", task.CheckerName),
        }
    }

    // Context with timeout
    ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
    defer cancel()

    // Execute check
    result := c.Check(ctx, task.Target, task.Params)

    // Log slow checks
    if result.Duration > 5*time.Second {
        log.Printf("[WARN] slow check: checker=%s target=%s duration=%s",
            task.CheckerName, task.Target, result.Duration)
    }

    return result
}

Scheduling Engine

// internal/scheduler/scheduler.go

package scheduler

import (
    "context"
    "fmt"
    "log"
    "sync"
    "time"

    "sre-inspector/internal/checker"
    "sre-inspector/internal/executor"
)

// ScheduledJob scheduled inspection task
type ScheduledJob struct {
    Name         string                       // task name
    Cron         string                       // cron expression
    Checkers     []string                     // checker list to run
    Targets      []string                     // target host list
    Params       map[string]map[string]string // params per checker
}

// Scheduler scheduling engine
type Scheduler struct {
    jobs     []*ScheduledJob
    executor *executor.Executor
    alertCh  chan checker.CheckResult // alert channel
    mu       sync.Mutex
    running  bool
}

// NewScheduler creates a scheduler
func NewScheduler(exec *executor.Executor, alertCh chan checker.CheckResult) *Scheduler {
    return &Scheduler{
        executor: exec,
        alertCh:  alertCh,
    }
}

// AddJob adds a scheduled task
func (s *Scheduler) AddJob(job *ScheduledJob) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.jobs = append(s.jobs, job)
}

// Start starts the scheduler
func (s *Scheduler) Start(ctx context.Context) {
    s.mu.Lock()
    s.running = true
    s.mu.Unlock()

    for _, job := range s.jobs {
        go s.runJob(ctx, job)
    }
    log.Printf("Scheduler started, %d scheduled tasks", len(s.jobs))
}

// runJob runs a single scheduled task
func (s *Scheduler) runJob(ctx context.Context, job *ScheduledJob) {
    interval, err := parseCronInterval(job.Cron)
    if err != nil {
        log.Printf("[ERROR] failed to parse cron: job=%s cron=%s err=%v", job.Name, job.Cron, err)
        return
    }

    ticker := time.NewTicker(interval)
    defer ticker.Stop()

    log.Printf("[INFO] job %s registered, interval: %v", job.Name, interval)

    for {
        select {
        case <-ctx.Done():
            log.Printf("[INFO] job %s stopped", job.Name)
            return
        case <-ticker.C:
            log.Printf("[INFO] starting inspection: %s", job.Name)
            s.executeJob(job)
        }
    }
}

// executeJob executes one full inspection run
func (s *Scheduler) executeJob(job *ScheduledJob) {
    start := time.Now()

    // Build task list: checker x target cartesian product
    var tasks []executor.Task
    for _, checkerName := range job.Checkers {
        params := job.Params[checkerName]
        for _, target := range job.Targets {
            tasks = append(tasks, executor.Task{
                CheckerName: checkerName,
                Target:      target,
                Params:      params,
            })
        }
    }

    log.Printf("[INFO] job %s: %d check items total", job.Name, len(tasks))

    // Batch execute
    results := s.executor.RunBatch(tasks)

    // Summarize
    var okCount, warnCount, critCount, unknownCount int
    for _, r := range results {
        switch r.Status {
        case checker.StatusOK:
            okCount++
        case checker.StatusWarning:
            warnCount++
            s.alertCh <- r
        case checker.StatusCritical:
            critCount++
            s.alertCh <- r
        default:
            unknownCount++
            s.alertCh <- r
        }
    }

    log.Printf("[INFO] job %s done: duration=%s ok=%d warning=%d critical=%d unknown=%d",
        job.Name, time.Since(start), okCount, warnCount, critCount, unknownCount)
}

// parseCronInterval simplified cron parser
func parseCronInterval(cronExpr string) (time.Duration, error) {
    var num int
    var unit string
    _, err := fmt.Sscanf(cronExpr, "every %d%c", &num, &unit)
    if err != nil {
        return time.Hour, nil
    }
    switch unit {
    case 'm':
        return time.Duration(num) * time.Minute, nil
    case 'h':
        return time.Duration(num) * time.Hour, nil
    default:
        return time.Hour, fmt.Errorf("unsupported time unit: %c", unit)
    }
}

Alert Engine

// internal/alert/alert.go

package alert

import (
    "fmt"
    "log"
    "strings"
    "sync"
    "time"

    "sre-inspector/internal/checker"
)

// AlertConfig alert configuration
type AlertConfig struct {
    WebhookURL     string        // webhook notification URL
    EmailTo        []string      // email recipients
    DingtalkToken  string        // DingTalk bot token
    RepeatInterval time.Duration // dedup interval
}

// AlertEngine alert engine
type AlertEngine struct {
    config       AlertConfig
    recentAlerts sync.Map // dedup cache: key="checker:target" -> lastAlertTime
}

// NewAlertEngine creates an alert engine
func NewAlertEngine(config AlertConfig) *AlertEngine {
    return &AlertEngine{config: config}
}

// Process processes a check result for alerting
func (a *AlertEngine) Process(result checker.CheckResult) {
    if result.Status == checker.StatusOK {
        return
    }

    key := fmt.Sprintf("%s:%s", result.CheckerName, result.Target)

    // Dedup: same check+target does not alert again within RepeatInterval
    if lastTime, ok := a.recentAlerts.Load(key); ok {
        if time.Since(lastTime.(time.Time)) < a.config.RepeatInterval {
            return
        }
    }
    a.recentAlerts.Store(key, time.Now())

    title, message := a.buildMessage(result)
    log.Printf("[ALERT] %s: %s", title, message)

    if a.config.WebhookURL != "" {
        a.sendWebhook(title, message, result)
    }
    if a.config.DingtalkToken != "" {
        a.sendDingtalk(title, message)
    }
}

// buildMessage builds alert message
func (a *AlertEngine) buildMessage(result checker.CheckResult) (string, string) {
    severity := "WARNING"
    if result.Status == checker.StatusCritical {
        severity = "CRITICAL"
    } else if result.Status == checker.StatusUnknown {
        severity = "UNKNOWN"
    }

    title := fmt.Sprintf("[%s] Inspection Alert: %s @ %s", severity, result.CheckerName, result.Target)

    var sb strings.Builder
    sb.WriteString(fmt.Sprintf("**Check**: %s\n", result.CheckerName))
    sb.WriteString(fmt.Sprintf("**Target**: %s\n", result.Target))
    sb.WriteString(fmt.Sprintf("**Status**: %s\n", severity))
    sb.WriteString(fmt.Sprintf("**Details**: %s\n", result.Message))
    sb.WriteString(fmt.Sprintf("**Time**: %s\n", result.CheckedAt.Format("2006-01-02 15:04:05")))
    sb.WriteString(fmt.Sprintf("**Duration**: %s\n", result.Duration))

    if len(result.Metrics) > 0 {
        sb.WriteString("**Metrics**:\n")
        for k, v := range result.Metrics {
            sb.WriteString(fmt.Sprintf("  - %s: %.2f\n", k, v))
        }
    }

    return title, sb.String()
}

func (a *AlertEngine) sendWebhook(title, message string, result checker.CheckResult) {
    log.Printf("[WEBHOOK] sending alert to %s: %s", a.config.WebhookURL, title)
}

func (a *AlertEngine) sendDingtalk(title, message string) {
    log.Printf("[DINGTALK] sending alert: %s", title)
}

// Start starts the alert engine, listening on the result channel
func (a *AlertEngine) Start(alertCh <-chan checker.CheckResult) {
    for result := range alertCh {
        a.Process(result)
    }
}

Configuration File

# configs/inspector.yaml

# Execution engine config
executor:
  max_workers: 20          # max concurrency
  timeout: 30s             # per-check timeout

# Alert config
alert:
  webhook_url: "https://hooks.example.com/inspector"
  dingtalk_token: ""
  repeat_interval: 30m     # same alert not repeated within 30 min

# Storage config
storage:
  type: sqlite             # sqlite or postgres
  dsn: "/var/lib/inspector/inspector.db"

# Asset inventory
targets:
  - host: "10.0.0.5"
    name: "web-01"
    tags: ["web", "production"]
  - host: "10.0.0.6"
    name: "web-02"
    tags: ["web", "production"]
  - host: "10.0.0.10"
    name: "db-01"
    tags: ["database", "production"]

# Checker config
checkers:
  - name: disk
    enabled: true
    params:
      warning: "80"
      critical: "95"
  
  - name: cpu
    enabled: true
    params:
      warning: "70"
      critical: "90"
      interval: "5m"
  
  - name: memory
    enabled: true
    params:
      warning: "80"
      critical: "95"
  
  - name: certificate
    enabled: true
    params:
      warning: "30"
      critical: "7"
      domains: "api.example.com,admin.example.com"
  
  - name: connection
    enabled: true
    params:
      warning: "5000"
      critical: "10000"
  
  - name: process
    enabled: true
    params:
      processes: "nginx,mysql,redis"

# Scheduled jobs
jobs:
  - name: "Full inspection"
    schedule: "every 6h"
    checkers: ["disk", "cpu", "memory", "certificate", "connection", "process"]
    target_tags: ["production"]
  
  - name: "Database inspection"
    schedule: "every 1h"
    checkers: ["disk", "memory", "process"]
    target_tags: ["database"]
  
  - name: "Certificate daily check"
    schedule: "every 24h"
    checkers: ["certificate"]
    target_tags: ["production"]

Main Entry Point

// cmd/inspector/main.go

package main

import (
    "context"
    "flag"
    "log"
    "os"
    "os/signal"
    "syscall"
    "time"

    "sre-inspector/internal/alert"
    "sre-inspector/internal/checker"
    "sre-inspector/internal/config"
    "sre-inspector/internal/executor"
    "sre-inspector/internal/report"
    "sre-inspector/internal/scheduler"
    "sre-inspector/internal/storage"
)

func main() {
    configPath := flag.String("config", "configs/inspector.yaml", "config file path")
    once := flag.Bool("once", false, "run inspection once and exit")
    flag.Parse()

    // Load config
    cfg, err := config.Load(*configPath)
    if err != nil {
        log.Fatalf("Failed to load config: %v", err)
    }

    // Initialize plugin registry
    registry := checker.NewRegistry()
    registry.RegisterBuiltin()
    log.Printf("Registered checkers: %v", registry.List())

    // Create alert channel
    alertCh := make(chan checker.CheckResult, 1000)

    // Initialize execution engine
    exec := executor.NewExecutor(
        registry,
        cfg.Executor.MaxWorkers,
        cfg.Executor.Timeout,
    )

    // Initialize alert engine
    alertEngine := alert.NewAlertEngine(cfg.Alert)
    go alertEngine.Start(alertCh)

    // Initialize storage
    store, err := storage.New(cfg.Storage)
    if err != nil {
        log.Fatalf("Failed to init storage: %v", err)
    }
    defer store.Close()

    // Initialize report generator
    reportGen := report.NewGenerator(store)

    // One-shot mode
    if *once {
        runOnce(cfg, exec, alertCh, store, reportGen)
        return
    }

    // Initialize scheduler
    sched := scheduler.NewScheduler(exec, alertCh)
    for _, jobCfg := range cfg.Jobs {
        job := buildJobFromConfig(jobCfg, cfg)
        sched.AddJob(job)
    }

    // Start scheduler
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    sched.Start(ctx)

    log.Println("Inspection platform started, press Ctrl+C to exit")

    // Wait for exit signal
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
    <-sigCh

    log.Println("Shutting down...")
    cancel()
    time.Sleep(2 * time.Second)
    log.Println("Exited")
}

// runOnce runs one inspection cycle
func runOnce(cfg *config.Config, exec *executor.Executor, alertCh chan checker.CheckResult, store *storage.Storage, reportGen *report.Generator) {
    log.Println("Starting one-shot inspection...")
    start := time.Now()

    var tasks []executor.Task
    for _, checkerCfg := range cfg.Checkers {
        if !checkerCfg.Enabled {
            continue
        }
        for _, target := range cfg.Targets {
            tasks = append(tasks, executor.Task{
                CheckerName: checkerCfg.Name,
                Target:      target.Host,
                Params:      checkerCfg.Params,
            })
        }
    }

    results := exec.RunBatch(tasks)
    store.SaveResults(results)

    for _, r := range results {
        if r.Status != checker.StatusOK {
            alertCh <- r
        }
    }

    reportPath := reportGen.GenerateHTML(results)
    log.Printf("Inspection complete: duration=%s report=%s", time.Since(start), reportPath)

    var ok, warn, crit, unk int
    for _, r := range results {
        switch r.Status {
        case checker.StatusOK:
            ok++
        case checker.StatusWarning:
            warn++
        case checker.StatusCritical:
            crit++
        default:
            unk++
        }
    }
    log.Printf("Summary: ok=%d warning=%d critical=%d unknown=%d", ok, warn, crit, unk)
}

Inspection Report Generation

The final output of inspection is a report. Reports are for humans, not machines — they need to be intuitive, readable, and quick to locate problems.

HTML Report Structure

// internal/report/report.go

package report

import (
    "fmt"
    "html/template"
    "os"
    "path/filepath"
    "time"

    "sre-inspector/internal/checker"
)

type Generator struct {
    tmpl *template.Template
}

func NewGenerator(store *storage.Storage) *Generator {
    return &Generator{}
}

// ReportData report data structure
type ReportData struct {
    GeneratedAt   time.Time
    TotalCount    int
    OKCount       int
    WarningCount  int
    CriticalCount int
    UnknownCount  int
    Results       []checker.CheckResult
    Summary       map[string]map[string]int
}

// GenerateHTML generates an HTML report
func (g *Generator) GenerateHTML(results []checker.CheckResult) string {
    data := g.prepareData(results)

    tmpl := template.Must(template.New("report").Parse(reportTemplate))

    filename := fmt.Sprintf("inspection_%s.html", time.Now().Format("20060102_150405"))
    filepath := filepath.Join("/var/lib/inspector/reports", filename)

    os.MkdirAll(filepath, 0755)
    f, err := os.Create(filepath)
    if err != nil {
        return ""
    }
    defer f.Close()

    tmpl.Execute(f, data)
    return filepath
}

func (g *Generator) prepareData(results []checker.CheckResult) ReportData {
    data := ReportData{
        GeneratedAt: time.Now(),
        TotalCount:  len(results),
        Results:     results,
        Summary:     make(map[string]map[string]int),
    }

    for _, r := range results {
        if data.Summary[r.Target] == nil {
            data.Summary[r.Target] = make(map[string]int)
        }
        data.Summary[r.Target][r.Status.String()]++
        switch r.Status {
        case checker.StatusOK:
            data.OKCount++
        case checker.StatusWarning:
            data.WarningCount++
        case checker.StatusCritical:
            data.CriticalCount++
        default:
            data.UnknownCount++
        }
    }
    return data
}

const reportTemplate = `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Inspection Report - {{.GeneratedAt.Format "2006-01-02 15:04"}}</title>
    <style>
        body { font-family: -apple-system, sans-serif; margin: 40px; }
        .summary { display: flex; gap: 20px; margin-bottom: 30px; }
        .card { padding: 20px; border-radius: 8px; color: white; }
        .ok { background: #4caf50; }
        .warning { background: #ff9800; }
        .critical { background: #f44336; }
        .unknown { background: #9e9e9e; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background: #f5f5f5; }
        .status-ok { color: #4caf50; }
        .status-warning { color: #ff9800; }
        .status-critical { color: #f44336; }
        .status-unknown { color: #9e9e9e; }
    </style>
</head>
<body>
    <h1>Inspection Report</h1>
    <p>Generated: {{.GeneratedAt.Format "2006-01-02 15:04:05"}}</p>

    <div class="summary">
        <div class="card ok"><h2>{{.OKCount}}</h2><p>OK</p></div>
        <div class="card warning"><h2>{{.WarningCount}}</h2><p>Warning</p></div>
        <div class="card critical"><h2>{{.CriticalCount}}</h2><p>Critical</p></div>
        <div class="card unknown"><h2>{{.UnknownCount}}</h2><p>Unknown</p></div>
    </div>

    <h2>Detailed Results</h2>
    <table>
        <tr>
            <th>Checker</th>
            <th>Target</th>
            <th>Status</th>
            <th>Details</th>
            <th>Duration</th>
        </tr>
        {{range .Results}}
        <tr>
            <td>{{.CheckerName}}</td>
            <td>{{.Target}}</td>
            <td class="status-{{.Status.String | toLower}}">{{.Status}}</td>
            <td>{{.Message}}</td>
            <td>{{.Duration}}</td>
        </tr>
        {{end}}
    </table>
</body>
</html>
`

Production Deployment

Binary Deployment

# Cross-compile
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/inspector ./cmd/inspector

# Deploy to server
scp bin/inspector configs/inspector.yaml user@server:/opt/inspector/

# Create systemd service
cat > /etc/systemd/system/inspector.service << 'EOF'
[Unit]
Description=SRE Inspection Platform
After=network.target

[Service]
Type=simple
User=inspector
WorkingDirectory=/opt/inspector
ExecStart=/opt/inspector/inspector -config /opt/inspector/configs/inspector.yaml
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable inspector
systemctl start inspector

SSH Keyless Configuration

Inspection requires SSH access to target machines. Use key authentication, not passwords:

# Generate a dedicated key on the inspection server
ssh-keygen -t ed25519 -f /home/inspector/.ssh/inspector_key -N ""

# Distribute public key to target machines
for host in 10.0.0.{5..50}; do
    ssh-copy-id -i /home/inspector/.ssh/inspector_key.pub inspector@$host
done

# Configure SSH aliases
cat >> /home/inspector/.ssh/config << 'EOF'
Host *
    IdentityFile ~/.ssh/inspector_key
    StrictHostKeyChecking no
    ConnectTimeout 5
    ServerAliveInterval 30
EOF

Security Considerations

  1. Least privilege: The inspection user should have read-only permissions. Do not run as root. Create a dedicated inspector user with a sudo whitelist for specific read-only commands.
# /etc/sudoers.d/inspector
inspector ALL=(ALL) NOPASSWD: /usr/bin/df, /usr/bin/free, /usr/bin/uptime, /usr/bin/ss, /usr/bin/ps
  1. SSH key protection: If the inspection server’s private key leaks, all target machines are compromised. Private key file permissions must be 600. Ideally, store keys in a hardware security module or key management service.

  2. Inspection data sensitivity: Results contain host info, process lists, network connections — sensitive operational data. Store reports in controlled directories and clean up expired reports regularly.

  3. Network isolation: The inspection server can access all target machines, making it a high-value attack target. Place it in a management network with restricted inbound access.

Common Check Item Implementation Approaches

Certificate Expiry Check

// Core logic: TLS connect to the target domain, read the certificate chain, check NotAfter
func checkCertificate(domain string, warnDays, critDays int) CheckResult {
    conn, err := tls.Dial("tcp", domain+":443", &tls.Config{
        InsecureSkipVerify: true, // We want to check expired certs, not refuse them
    })
    if err != nil {
        return CheckResult{Status: StatusUnknown, Message: fmt.Sprintf("TLS connection failed: %v", err)}
    }
    defer conn.Close()

    cert := conn.ConnectionState().PeerCertificates[0]
    daysLeft := int(time.Until(cert.NotAfter).Hours() / 24)

    status := StatusOK
    message := fmt.Sprintf("Certificate expires in %d days (expiry: %s)", daysLeft, cert.NotAfter.Format("2006-01-02"))

    if daysLeft <= critDays {
        status = StatusCritical
        message = fmt.Sprintf("Certificate expiring soon! Only %d days left (expiry: %s)", daysLeft, cert.NotAfter.Format("2006-01-02"))
    } else if daysLeft <= warnDays {
        status = StatusWarning
        message = fmt.Sprintf("Certificate expires in %d days (expiry: %s)", daysLeft, cert.NotAfter.Format("2006-01-02"))
    }

    return CheckResult{
        Status:  status,
        Message:  message,
        Metrics:  map[string]float64{"days_until_expiry": float64(daysLeft)},
    }
}

Critical Process Check

// Check if required processes are running via SSH pgrep
func checkProcess(ctx context.Context, target string, processNames []string) CheckResult {
    var missing []string
    for _, name := range processNames {
        cmd := exec.CommandContext(ctx, "ssh", target, fmt.Sprintf("pgrep -x %s", name))
        if err := cmd.Run(); err != nil {
            missing = append(missing, name)
        }
    }

    if len(missing) > 0 {
        return CheckResult{
            Status:  StatusCritical,
            Message:  fmt.Sprintf("Processes not running: %s", strings.Join(missing, ", ")),
        }
    }
    return CheckResult{Status: StatusOK, Message: "All critical processes running"}
}

TCP Connection Count Check

// Count current ESTABLISHED connections via SSH ss
func checkConnectionCount(ctx context.Context, target string, warn, crit int) CheckResult {
    cmd := exec.CommandContext(ctx, "ssh", target, "ss -tn state established | wc -l")
    output, err := cmd.Output()
    if err != nil {
        return CheckResult{Status: StatusUnknown, Message: fmt.Sprintf("Execution failed: %v", err)}
    }

    count := 0
    fmt.Sscanf(strings.TrimSpace(string(output)), "%d", &count)

    status := StatusOK
    message := fmt.Sprintf("Current ESTABLISHED connections: %d", count)
    if count >= crit {
        status = StatusCritical
        message = fmt.Sprintf("Connection count critical: %d >= %d", count, crit)
    } else if count >= warn {
        status = StatusWarning
        message = fmt.Sprintf("Connection count warning: %d >= %d", count, warn)
    }

    return CheckResult{
        Status:  status,
        Message:  message,
        Metrics:  map[string]float64{"established_connections": float64(count)},
    }
}

Relationship with Existing Monitoring Systems

The inspection platform and monitoring systems like Prometheus are not competing — they serve different purposes:

DimensionInspection PlatformPrometheus
Check frequencyLow (hourly/daily)High (every 15-60s)
Check scopeComprehensive (disk+cert+process+config)Metrics-focused (CPU/memory/traffic)
AlertingInspection report + instant alertsReal-time alerts
Use casePeriodic health checks, compliance auditsReal-time monitoring, dynamic alerting
Data retentionLong-term (monthly/quarterly trends)Mid-term (typically 15-90 days)

In short: Prometheus is an EKG monitor, running 24/7; the inspection platform is an annual physical exam. They complement each other and cannot replace each other.

The inspection platform can do things Prometheus is not good at:

  • Certificate expiry checks (requires active TLS connection)
  • Config file compliance checks (requires reading file contents)
  • Cross-machine comparison (distribution of the same metric across all hosts)
  • Offline report generation (no long-running monitoring service needed)

Future Directions

The architecture supports several extension paths:

  1. Agent mode: Currently SSH-based, suitable for small-to-mid scale. Beyond 500 machines, SSH overhead becomes significant. A lightweight agent can be deployed to target machines, reporting results via gRPC.

  2. Web management UI: Currently configured via YAML. A Web UI can enable visual check item configuration, historical trend charts, and one-click report generation.

  3. Auto-remediation: Beyond alerting, trigger auto-fix scripts when issues are found — clean logs when disk is full, restart crashed processes. But auto-remediation must be cautious — wrong remediation actions are more dangerous than the original problem.

  4. Trend analysis: With historical data in the database, trend analysis becomes possible. “What is the disk usage growth rate over the past 30 days? At current rate, how many days until 95%?” This kind of predictive analysis is valuable for capacity planning.

  5. Compliance checks: Add compliance rules to check items — password policy compliance, SSH root login disabled, firewall rules correct. Integrate security compliance scanning into daily inspection.

Summary

The core value of an automated inspection platform: turning repetitive manual inspection into automated system engineering, freeing ops engineers from “checking every day” to focus on things that truly need human judgment.

Key architecture decisions:

  • Plugin architecture: Checker interface + Registry, adding check items without modifying the main program. Good extensibility.
  • Worker Pool concurrency: goroutine + channel to control concurrency. 200 machines with 20 concurrent workers finish in 3 minutes — 10x faster than serial.
  • Alert deduplication: Same issue does not alert again within the suppression window, preventing alert storms. In practice at 200-machine scale, a disk-full alert went from 50 notifications down to 1.
  • Config-driven: Check items, thresholds, target hosts, and schedules all in a YAML file. No recompilation needed for config changes.
  • Go single-binary deployment: A 15MB binary + a YAML config = complete deployment. No environment management headaches like Python.

Pitfalls encountered:

  • SSH connection timeout must be short (5 seconds). Otherwise, unreachable targets drag the entire inspection down. ConnectTimeout=5 with ServerAliveInterval=30 is a stable combination.
  • More goroutines is not better. 200 goroutines for 200 machines will momentarily exhaust target machines’ MaxStartups. 20 concurrent is the sweet spot for 200 machines.
  • Alert dedup window must be tuned per check type. 30 minutes is fine for disk issues, but certificate checks that run daily need a 24-hour window, or you get 24 identical alerts per day.
  • df output format differs across Linux distros. CentOS 7 and Ubuntu 22.04 have different column orders. Use df --output=pcent,target to force a consistent format and avoid parsing issues.

One final piece of experience: the inspection platform does not replace monitoring systems — it complements them. Prometheus handles real-time problem detection; the inspection platform handles periodic comprehensive health checks. Combined, ops can truly “know the state of things.”

References & Acknowledgments

The following resources were consulted during the writing of this article. Credit to the original authors:

  1. From Zero to One: Modular Architecture Design for Python Network Automated Inspection — CSDN, modular inspection architecture design, connection management and data processing module implementation reference
  2. AI Agent Automated Ops System: Evolution from Manual Inspection to Autonomous Operations — Tencent Cloud Developer Community, layered design and modular practices for automated ops systems
  3. Go Concurrency Tour — Go Official Tour, goroutine and channel concurrency model fundamentals
  4. robfig/cron: Go cron Library — GitHub, cron expression parsing and scheduled task scheduling for Go