// Copyright 2021 Molecula Corp. All rights reserved. package pilosa import ( "bytes" "encoding/json" "fmt" "net/http" "strconv" "strings" "sync" "time" "github.com/molecula/featurebase/v3/logger" "github.com/pkg/errors" ) // Default version check URL. const ( defaultVersionCheckURL = "https://diagnostics.pilosa.com/v0/version" ) type versionResponse struct { Version string `json:"version"` Message string `json:"message"` } // diagnosticsCollector represents a collector/sender of diagnostics data. type diagnosticsCollector struct { mu sync.Mutex host string VersionURL string version string lastVersion string startTime int64 start time.Time metrics map[string]interface{} client *http.Client Logger logger.Logger server *Server } // newDiagnosticsCollector returns a new DiagnosticsCollector given an addr in the format "hostname:port". func newDiagnosticsCollector(host string) *diagnosticsCollector { // nolint: unparam return &diagnosticsCollector{ host: host, VersionURL: defaultVersionCheckURL, startTime: time.Now().Unix(), start: time.Now(), client: &http.Client{Timeout: 10 * time.Second}, metrics: make(map[string]interface{}), Logger: logger.NopLogger, } } // SetVersion of locally running Pilosa Cluster to check against master. func (d *diagnosticsCollector) SetVersion(v string) { d.version = v d.Set("Version", v) } // Flush sends the current metrics. func (d *diagnosticsCollector) Flush() error { d.mu.Lock() defer d.mu.Unlock() d.metrics["Uptime"] = (time.Now().Unix() - d.startTime) buf, err := d.encode() if err != nil { return errors.Wrap(err, "encoding") } req, err := http.NewRequest("POST", d.host, bytes.NewReader(buf)) if err != nil { return errors.Wrap(err, "making new request") } req.Header.Set("Content-Type", "application/json") resp, err := d.client.Do(req) if err != nil { return errors.Wrap(err, "posting") } // Intentionally ignoring response body, as user does not need to be notified of error. defer resp.Body.Close() return nil } // CheckVersion of the local build against Pilosa master. func (d *diagnosticsCollector) CheckVersion() error { var rsp versionResponse req, err := http.NewRequest("GET", d.VersionURL, nil) if err != nil { return errors.Wrap(err, "making request") } resp, err := d.client.Do(req) if err != nil { return errors.Wrap(err, "getting version") } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("http: status=%d", resp.StatusCode) } else if err := json.NewDecoder(resp.Body).Decode(&rsp); err != nil { return fmt.Errorf("json decode: %s", err) } // If version has not changed since the last check, return if rsp.Version == d.lastVersion { return nil } d.lastVersion = rsp.Version if err := d.compareVersion(rsp.Version); err != nil { d.Logger.Infof("%s\n", err.Error()) } return nil } // compareVersion check version strings. func (d *diagnosticsCollector) compareVersion(value string) error { currentVersion := versionSegments(value) localVersion := versionSegments(d.version) if localVersion[0] < currentVersion[0] { //Major return fmt.Errorf("you are running Pilosa %s, a newer version (%s) is available: https://github.com/molecula/featurebase/releases", d.version, value) } else if localVersion[1] < currentVersion[1] && localVersion[0] == currentVersion[0] { // Minor return fmt.Errorf("you are running Pilosa %s, the latest minor release is %s: https://github.com/molecula/featurebase/releases", d.version, value) } else if localVersion[2] < currentVersion[2] && localVersion[0] == currentVersion[0] && localVersion[1] == currentVersion[1] { // Patch return fmt.Errorf("there is a new patch release of Pilosa available: %s: https://github.com/molecula/featurebase/releases", value) } return nil } // Encode metrics maps into the json message format. func (d *diagnosticsCollector) encode() ([]byte, error) { return json.Marshal(d.metrics) } // Set adds a key value metric. func (d *diagnosticsCollector) Set(name string, value interface{}) { switch v := value.(type) { case string: if v == "" { // Do not set empty string return } } d.mu.Lock() defer d.mu.Unlock() d.metrics[name] = value } // logErr logs the error and returns true if an error exists func (d *diagnosticsCollector) logErr(err error) bool { if err != nil { d.Logger.Errorf("%v", err) return true } return false } // EnrichWithCPUInfo adds CPU information to the diagnostics payload. func (d *diagnosticsCollector) EnrichWithCPUInfo() { d.Set("CPUArch", d.server.systemInfo.CPUArch()) } // EnrichWithOSInfo adds OS information to the diagnostics payload. func (d *diagnosticsCollector) EnrichWithOSInfo() { uptime, err := d.server.systemInfo.Uptime() if !d.logErr(err) { d.Set("HostUptime", uptime) } platform, err := d.server.systemInfo.Platform() if !d.logErr(err) { d.Set("OSPlatform", platform) } family, err := d.server.systemInfo.Family() if !d.logErr(err) { d.Set("OSFamily", family) } version, err := d.server.systemInfo.OSVersion() if !d.logErr(err) { d.Set("OSVersion", version) } kernelVersion, err := d.server.systemInfo.KernelVersion() if !d.logErr(err) { d.Set("OSKernelVersion", kernelVersion) } } // EnrichWithMemoryInfo adds memory information to the diagnostics payload. func (d *diagnosticsCollector) EnrichWithMemoryInfo() { memFree, err := d.server.systemInfo.MemFree() if !d.logErr(err) { d.Set("MemFree", memFree) } memTotal, err := d.server.systemInfo.MemTotal() if !d.logErr(err) { d.Set("MemTotal", memTotal) } memUsed, err := d.server.systemInfo.MemUsed() if !d.logErr(err) { d.Set("MemUsed", memUsed) } } // EnrichWithSchemaProperties adds schema info to the diagnostics payload. func (d *diagnosticsCollector) EnrichWithSchemaProperties() { var numShards uint64 numFields := 0 numIndexes := 0 bsiFieldCount := 0 timeQuantumEnabled := false for _, index := range d.server.holder.Indexes() { numShards += index.AvailableShards(includeRemote).Count() numIndexes++ for _, field := range index.Fields() { numFields++ if field.Type() == FieldTypeInt || field.Type() == FieldTypeDecimal || field.Type() == FieldTypeTimestamp { bsiFieldCount++ } if field.TimeQuantum() != "" { timeQuantumEnabled = true } } } d.Set("NumIndexes", numIndexes) d.Set("NumFields", numFields) d.Set("NumShards", numShards) d.Set("BSIFieldCount", bsiFieldCount) d.Set("TimeQuantumEnabled", timeQuantumEnabled) } // versionSegments returns the numeric segments of the version as a slice of ints. func versionSegments(segments string) []int { segments = strings.Trim(segments, "v") segments = strings.Split(segments, "-")[0] s := strings.Split(segments, ".") segmentSlice := make([]int, len(s)) for i, v := range s { segmentSlice[i], _ = strconv.Atoi(v) } return segmentSlice } // SystemInfo collects information about the host OS. type SystemInfo interface { Uptime() (uint64, error) Platform() (string, error) Family() (string, error) OSVersion() (string, error) KernelVersion() (string, error) MemFree() (uint64, error) MemTotal() (uint64, error) MemUsed() (uint64, error) CPUModel() string CPUCores() (physical int, logical int, err error) CPUMHz() (int, error) CPUArch() string DiskCapacity(string) (uint64, error) } // newNopSystemInfo creates a no-op implementation of SystemInfo. func newNopSystemInfo() *nopSystemInfo { return &nopSystemInfo{} } // nopSystemInfo is a no-op implementation of SystemInfo. type nopSystemInfo struct { } // Uptime is a no-op implementation of SystemInfo.Uptime. func (n *nopSystemInfo) Uptime() (uint64, error) { return 0, nil } // Platform is a no-op implementation of SystemInfo.Platform. func (n *nopSystemInfo) Platform() (string, error) { return "", nil } // Family is a no-op implementation of SystemInfo.Family. func (n *nopSystemInfo) Family() (string, error) { return "", nil } // OSVersion is a no-op implementation of SystemInfo.OSVersion. func (n *nopSystemInfo) OSVersion() (string, error) { return "", nil } // KernelVersion is a no-op implementation of SystemInfo.KernelVersion. func (n *nopSystemInfo) KernelVersion() (string, error) { return "", nil } // MemFree is a no-op implementation of SystemInfo.MemFree. func (n *nopSystemInfo) MemFree() (uint64, error) { return 0, nil } // MemTotal is a no-op implementation of SystemInfo.MemTotal. func (n *nopSystemInfo) MemTotal() (uint64, error) { return 0, nil } // MemUsed is a no-op implementation of SystemInfo.MemUsed. func (n *nopSystemInfo) MemUsed() (uint64, error) { return 0, nil } // CPUArch returns the CPU architecture, such as amd64 func (n *nopSystemInfo) CPUArch() string { return "" } // CPUModel returns the CPU model string func (n *nopSystemInfo) CPUModel() string { return "unknown" } // CPUMHz returns the CPU clock speed func (n *nopSystemInfo) CPUMHz() (int, error) { return 0, nil } // CPUCores returns the number of CPU cores (physical or logical) func (n *nopSystemInfo) CPUCores() (physical, logical int, err error) { return 0, 0, nil } // DiskCapacity returns the disk capacity func (n *nopSystemInfo) DiskCapacity(path string) (uint64, error) { return 0, nil }