Add a demo host-metrics fixture
Pushes synthetic cairnobs.metrics/cairnobs.heartbeat-tagged records straight to ingest so a demo deployment's Hosts page and per-host detail page have CPU/memory/disk to show. Neither benchmark-fixture nor windows-fixture emits those two attributes -- only the real agent does -- so nothing else could populate that page. Superseded by /hack/demo-simulator in the next commit, which covers the same records as part of a whole fleet; kept because it is still the smallest thing that fills the Hosts page on its own.
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
// Command demo-hosts-fixture pushes synthetic cairnobs.metrics/
|
||||
// cairnobs.heartbeat-tagged LogRecords directly to ingest's gRPC
|
||||
// endpoint, one snapshot every -interval across -time-spread per
|
||||
// synthetic host, so a demo deployment's Hosts page and per-host detail
|
||||
// page (CPU/mem/disk charts) have something to show. Neither
|
||||
// /hack/benchmark-fixture nor /hack/windows-fixture emit these two
|
||||
// attributes -- only the real agent does
|
||||
// (agent/cairnobs-agent/src/main.rs's send_metrics/send_heartbeat) --
|
||||
// so this fills that gap for demo/exploration data specifically, the
|
||||
// same use case benchmark-fixture's -time-spread flag was already added
|
||||
// for.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
logsv1 "github.com/cairnobs/cairnobs/proto/sentry/logs/v1"
|
||||
)
|
||||
|
||||
// Mirrors hack/benchmark-fixture's host/service pools so the same
|
||||
// hosts show up consistently across both the raw log fixture and this
|
||||
// one -- a viewer clicking from a host's log lines to its metrics page
|
||||
// (or vice versa) sees the same host, not a disjoint set.
|
||||
var (
|
||||
hosts = []string{"host-01", "host-02", "host-03", "host-04", "host-05", "host-06", "host-07", "host-08"}
|
||||
hostServices = map[string]string{
|
||||
"host-01": "api", "host-02": "api", "host-03": "web", "host-04": "web",
|
||||
"host-05": "worker", "host-06": "worker", "host-07": "db", "host-08": "auth",
|
||||
}
|
||||
osNames = []string{"Ubuntu 24.04.1 LTS", "Debian GNU/Linux 13 (trixie)", "Ubuntu 22.04.5 LTS"}
|
||||
kernelVersions = []string{"6.8.0-45-generic", "6.1.0-25-amd64", "5.15.0-118-generic"}
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := flag.String("addr", "localhost:4317", "ingest gRPC address")
|
||||
caFile := flag.String("ca", "../dev-certs/out/ca.pem", "CA cert path")
|
||||
certFile := flag.String("cert", "../dev-certs/out/client.pem", "client cert path")
|
||||
keyFile := flag.String("key", "../dev-certs/out/client-key.pem", "client key path")
|
||||
timeSpread := flag.Duration("time-spread", 24*time.Hour, "spread metrics snapshots uniformly across [now-spread, now], one per interval per host")
|
||||
interval := flag.Duration("interval", 5*time.Minute, "spacing between synthetic metrics snapshots per host")
|
||||
flag.Parse()
|
||||
|
||||
tlsConf, err := loadTLSConfig(*caFile, *certFile, *keyFile)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "loading TLS config:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(*addr, grpc.WithTransportCredentials(credentials.NewTLS(tlsConf)))
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "dialing ingest:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := logsv1.NewLogIngestClient(conn)
|
||||
|
||||
now := time.Now()
|
||||
var records []*logsv1.LogRecord
|
||||
for _, host := range hosts {
|
||||
service := hostServices[host]
|
||||
osName := osNames[rand.Intn(len(osNames))]
|
||||
kernel := kernelVersions[rand.Intn(len(kernelVersions))]
|
||||
cores := []int{2, 4, 8, 16}[rand.Intn(4)]
|
||||
memTotal := []int64{4, 8, 16, 32}[rand.Intn(4)] * 1024 * 1024 * 1024
|
||||
diskTotal := []int64{50, 100, 250, 500}[rand.Intn(4)] * 1024 * 1024 * 1024
|
||||
// A gently drifting baseline per host so the charts show real
|
||||
// variation instead of flat noise -- CPU/mem wander within a
|
||||
// plausible band across the snapshot window.
|
||||
cpuBase := 5 + rand.Float64()*40
|
||||
memBase := 0.3 + rand.Float64()*0.4
|
||||
diskBase := 0.2 + rand.Float64()*0.5
|
||||
uptime := int64(*timeSpread/time.Second) + rand.Int63n(30*86400)
|
||||
|
||||
for t := now.Add(-*timeSpread); t.Before(now); t = t.Add(*interval) {
|
||||
cpuPercent := clamp(cpuBase+rand.NormFloat64()*8, 0.5, 98)
|
||||
memUsed := int64(float64(memTotal) * clamp(memBase+rand.NormFloat64()*0.05, 0.05, 0.95))
|
||||
diskUsed := int64(float64(diskTotal) * clamp(diskBase+rand.NormFloat64()*0.02, 0.05, 0.9))
|
||||
|
||||
records = append(records, &logsv1.LogRecord{
|
||||
TimestampUnixNano: t.UnixNano(),
|
||||
Host: host,
|
||||
Service: service,
|
||||
Severity: logsv1.Severity_SEVERITY_INFO,
|
||||
Message: "host metrics",
|
||||
Attributes: map[string]string{
|
||||
"cairnobs.metrics": "true",
|
||||
"cpu_percent": fmt.Sprintf("%.2f", cpuPercent),
|
||||
"mem_used_bytes": strconv.FormatInt(memUsed, 10),
|
||||
"mem_total_bytes": strconv.FormatInt(memTotal, 10),
|
||||
"disk_used_bytes": strconv.FormatInt(diskUsed, 10),
|
||||
"disk_total_bytes": strconv.FormatInt(diskTotal, 10),
|
||||
"cpu_cores": strconv.Itoa(cores),
|
||||
"os_name": osName,
|
||||
"kernel_version": kernel,
|
||||
"arch": "x86_64",
|
||||
"uptime_seconds": strconv.FormatInt(uptime-int64(now.Sub(t).Seconds()), 10),
|
||||
"ipv4_addresses": fmt.Sprintf("10.0.0.%d", 10+rand.Intn(240)),
|
||||
"ipv6_addresses": "",
|
||||
},
|
||||
})
|
||||
records = append(records, &logsv1.LogRecord{
|
||||
TimestampUnixNano: t.UnixNano(),
|
||||
Host: host,
|
||||
Service: service,
|
||||
Severity: logsv1.Severity_SEVERITY_INFO,
|
||||
Message: "agent heartbeat",
|
||||
Attributes: map[string]string{"cairnobs.heartbeat": "true"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("pushing %d synthetic host-metrics/heartbeat records for %d hosts...\n", len(records), len(hosts))
|
||||
|
||||
const batchSize = 500
|
||||
for offset := 0; offset < len(records); offset += batchSize {
|
||||
end := offset + batchSize
|
||||
if end > len(records) {
|
||||
end = len(records)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
resp, err := client.PushBatch(ctx, &logsv1.PushBatchRequest{
|
||||
BatchId: fmt.Sprintf("demo-hosts-%d", offset),
|
||||
Records: records[offset:end],
|
||||
})
|
||||
cancel()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "PushBatch at offset %d failed: %v\n", offset, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("sent %d/%d (accepted %d)\n", end, len(records), resp.GetAccepted())
|
||||
}
|
||||
|
||||
fmt.Println("done")
|
||||
}
|
||||
|
||||
func clamp(v, lo, hi float64) float64 {
|
||||
if v < lo {
|
||||
return lo
|
||||
}
|
||||
if v > hi {
|
||||
return hi
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func loadTLSConfig(caFile, certFile, keyFile string) (*tls.Config, error) {
|
||||
caPEM, err := os.ReadFile(caFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading CA cert %s: %w", caFile, err)
|
||||
}
|
||||
caPool := x509.NewCertPool()
|
||||
if !caPool.AppendCertsFromPEM(caPEM) {
|
||||
return nil, fmt.Errorf("no valid certificates found in %s", caFile)
|
||||
}
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading client cert/key: %w", err)
|
||||
}
|
||||
|
||||
return &tls.Config{
|
||||
RootCAs: caPool,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user