378 lines
9.4 KiB
Go
378 lines
9.4 KiB
Go
|
package diagnostic
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"os/exec"
|
||
|
"runtime"
|
||
|
"sort"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
func findColonSeparatedPairs[V any](output string, keys []string, mapper func(string) (V, error)) map[string]V {
|
||
|
const (
|
||
|
memoryField = 1
|
||
|
memoryInformationFields = 2
|
||
|
)
|
||
|
|
||
|
lines := strings.Split(output, "\n")
|
||
|
pairs := make(map[string]V, 0)
|
||
|
|
||
|
// sort keys and lines to allow incremental search
|
||
|
sort.Strings(lines)
|
||
|
sort.Strings(keys)
|
||
|
|
||
|
// keeps track of the last key found
|
||
|
lastIndex := 0
|
||
|
|
||
|
for _, line := range lines {
|
||
|
if lastIndex == len(keys) {
|
||
|
// already found all keys no need to continue iterating
|
||
|
// over the other values
|
||
|
break
|
||
|
}
|
||
|
|
||
|
for index, key := range keys[lastIndex:] {
|
||
|
line = strings.TrimSpace(line)
|
||
|
if strings.HasPrefix(line, key) {
|
||
|
fields := strings.Split(line, ":")
|
||
|
if len(fields) < memoryInformationFields {
|
||
|
lastIndex = index + 1
|
||
|
|
||
|
break
|
||
|
}
|
||
|
|
||
|
field, err := mapper(strings.TrimSpace(fields[memoryField]))
|
||
|
if err != nil {
|
||
|
lastIndex = lastIndex + index + 1
|
||
|
|
||
|
break
|
||
|
}
|
||
|
|
||
|
pairs[key] = field
|
||
|
lastIndex = lastIndex + index + 1
|
||
|
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return pairs
|
||
|
}
|
||
|
|
||
|
func ParseDiskVolumeInformationOutput(output string, skipLines int, scale float64) ([]*DiskVolumeInformation, error) {
|
||
|
const (
|
||
|
diskFieldsMinimum = 3
|
||
|
nameField = 0
|
||
|
sizeMaximumField = 1
|
||
|
sizeCurrentField = 2
|
||
|
)
|
||
|
|
||
|
disksRaw := strings.Split(output, "\n")
|
||
|
disks := make([]*DiskVolumeInformation, 0)
|
||
|
|
||
|
if skipLines > len(disksRaw) || skipLines < 0 {
|
||
|
skipLines = 0
|
||
|
}
|
||
|
|
||
|
for _, disk := range disksRaw[skipLines:] {
|
||
|
if disk == "" {
|
||
|
// skip empty line
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
fields := strings.Fields(disk)
|
||
|
if len(fields) < diskFieldsMinimum {
|
||
|
return nil, fmt.Errorf("expected disk volume to have %d fields got %d: %w",
|
||
|
diskFieldsMinimum, len(fields), ErrInsuficientFields,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
name := fields[nameField]
|
||
|
|
||
|
sizeMaximum, err := strconv.ParseUint(fields[sizeMaximumField], 10, 64)
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
sizeCurrent, err := strconv.ParseUint(fields[sizeCurrentField], 10, 64)
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
diskInfo := NewDiskVolumeInformation(
|
||
|
name, uint64(float64(sizeMaximum)*scale), uint64(float64(sizeCurrent)*scale),
|
||
|
)
|
||
|
disks = append(disks, diskInfo)
|
||
|
}
|
||
|
|
||
|
if len(disks) == 0 {
|
||
|
return nil, ErrNoVolumeFound
|
||
|
}
|
||
|
|
||
|
return disks, nil
|
||
|
}
|
||
|
|
||
|
type OsInfo struct {
|
||
|
OsSystem string
|
||
|
Name string
|
||
|
OsVersion string
|
||
|
OsRelease string
|
||
|
Architecture string
|
||
|
}
|
||
|
|
||
|
func ParseUnameOutput(output string, system string) (*OsInfo, error) {
|
||
|
const (
|
||
|
osystemField = 0
|
||
|
nameField = 1
|
||
|
osVersionField = 2
|
||
|
osReleaseStartField = 3
|
||
|
osInformationFieldsMinimum = 6
|
||
|
darwin = "darwin"
|
||
|
)
|
||
|
|
||
|
architectureOffset := 2
|
||
|
if system == darwin {
|
||
|
architectureOffset = 1
|
||
|
}
|
||
|
|
||
|
fields := strings.Fields(output)
|
||
|
if len(fields) < osInformationFieldsMinimum {
|
||
|
return nil, fmt.Errorf("expected system information to have %d fields got %d: %w",
|
||
|
osInformationFieldsMinimum, len(fields), ErrInsuficientFields,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
architectureField := len(fields) - architectureOffset
|
||
|
osystem := fields[osystemField]
|
||
|
name := fields[nameField]
|
||
|
osVersion := fields[osVersionField]
|
||
|
osRelease := strings.Join(fields[osReleaseStartField:architectureField], " ")
|
||
|
architecture := fields[architectureField]
|
||
|
|
||
|
return &OsInfo{
|
||
|
osystem,
|
||
|
name,
|
||
|
osVersion,
|
||
|
osRelease,
|
||
|
architecture,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func ParseWinOperatingSystemInfo(
|
||
|
output string,
|
||
|
architectureKey string,
|
||
|
osSystemKey string,
|
||
|
osVersionKey string,
|
||
|
osReleaseKey string,
|
||
|
nameKey string,
|
||
|
) (*OsInfo, error) {
|
||
|
identity := func(s string) (string, error) { return s, nil }
|
||
|
|
||
|
keys := []string{architectureKey, osSystemKey, osVersionKey, osReleaseKey, nameKey}
|
||
|
pairs := findColonSeparatedPairs(
|
||
|
output,
|
||
|
keys,
|
||
|
identity,
|
||
|
)
|
||
|
|
||
|
architecture, exists := pairs[architectureKey]
|
||
|
if !exists {
|
||
|
return nil, fmt.Errorf("parsing os information: %w, key=%s", ErrKeyNotFound, architectureKey)
|
||
|
}
|
||
|
|
||
|
osSystem, exists := pairs[osSystemKey]
|
||
|
if !exists {
|
||
|
return nil, fmt.Errorf("parsing os information: %w, key=%s", ErrKeyNotFound, osSystemKey)
|
||
|
}
|
||
|
|
||
|
osVersion, exists := pairs[osVersionKey]
|
||
|
if !exists {
|
||
|
return nil, fmt.Errorf("parsing os information: %w, key=%s", ErrKeyNotFound, osVersionKey)
|
||
|
}
|
||
|
|
||
|
osRelease, exists := pairs[osReleaseKey]
|
||
|
if !exists {
|
||
|
return nil, fmt.Errorf("parsing os information: %w, key=%s", ErrKeyNotFound, osReleaseKey)
|
||
|
}
|
||
|
|
||
|
name, exists := pairs[nameKey]
|
||
|
if !exists {
|
||
|
return nil, fmt.Errorf("parsing os information: %w, key=%s", ErrKeyNotFound, nameKey)
|
||
|
}
|
||
|
|
||
|
return &OsInfo{osSystem, name, osVersion, osRelease, architecture}, nil
|
||
|
}
|
||
|
|
||
|
type FileDescriptorInformation struct {
|
||
|
FileDescriptorMaximum uint64
|
||
|
FileDescriptorCurrent uint64
|
||
|
}
|
||
|
|
||
|
func ParseSysctlFileDescriptorInformation(output string) (*FileDescriptorInformation, error) {
|
||
|
const (
|
||
|
openFilesField = 0
|
||
|
maxFilesField = 2
|
||
|
fileDescriptorLimitsFields = 3
|
||
|
)
|
||
|
|
||
|
fields := strings.Fields(output)
|
||
|
|
||
|
if len(fields) != fileDescriptorLimitsFields {
|
||
|
return nil,
|
||
|
fmt.Errorf(
|
||
|
"expected file descriptor information to have %d fields got %d: %w",
|
||
|
fileDescriptorLimitsFields,
|
||
|
len(fields),
|
||
|
ErrInsuficientFields,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
fileDescriptorCurrent, err := strconv.ParseUint(fields[openFilesField], 10, 64)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf(
|
||
|
"error parsing files current field '%s': %w",
|
||
|
fields[openFilesField],
|
||
|
err,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
fileDescriptorMaximum, err := strconv.ParseUint(fields[maxFilesField], 10, 64)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("error parsing files max field '%s': %w", fields[maxFilesField], err)
|
||
|
}
|
||
|
|
||
|
return &FileDescriptorInformation{fileDescriptorMaximum, fileDescriptorCurrent}, nil
|
||
|
}
|
||
|
|
||
|
func ParseFileDescriptorInformationFromKV(
|
||
|
output string,
|
||
|
fileDescriptorMaximumKey string,
|
||
|
fileDescriptorCurrentKey string,
|
||
|
) (*FileDescriptorInformation, error) {
|
||
|
mapper := func(field string) (uint64, error) {
|
||
|
return strconv.ParseUint(field, 10, 64)
|
||
|
}
|
||
|
|
||
|
pairs := findColonSeparatedPairs(output, []string{fileDescriptorMaximumKey, fileDescriptorCurrentKey}, mapper)
|
||
|
|
||
|
fileDescriptorMaximum, exists := pairs[fileDescriptorMaximumKey]
|
||
|
if !exists {
|
||
|
return nil, fmt.Errorf(
|
||
|
"parsing file descriptor information: %w, key=%s",
|
||
|
ErrKeyNotFound,
|
||
|
fileDescriptorMaximumKey,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
fileDescriptorCurrent, exists := pairs[fileDescriptorCurrentKey]
|
||
|
if !exists {
|
||
|
return nil, fmt.Errorf(
|
||
|
"parsing file descriptor information: %w, key=%s",
|
||
|
ErrKeyNotFound,
|
||
|
fileDescriptorCurrentKey,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
return &FileDescriptorInformation{fileDescriptorMaximum, fileDescriptorCurrent}, nil
|
||
|
}
|
||
|
|
||
|
type MemoryInformation struct {
|
||
|
MemoryMaximum uint64 // size in KB
|
||
|
MemoryCurrent uint64 // size in KB
|
||
|
}
|
||
|
|
||
|
func ParseMemoryInformationFromKV(
|
||
|
output string,
|
||
|
memoryMaximumKey string,
|
||
|
memoryAvailableKey string,
|
||
|
mapper func(field string) (uint64, error),
|
||
|
) (*MemoryInformation, error) {
|
||
|
pairs := findColonSeparatedPairs(output, []string{memoryMaximumKey, memoryAvailableKey}, mapper)
|
||
|
|
||
|
memoryMaximum, exists := pairs[memoryMaximumKey]
|
||
|
if !exists {
|
||
|
return nil, fmt.Errorf("parsing memory information: %w, key=%s", ErrKeyNotFound, memoryMaximumKey)
|
||
|
}
|
||
|
|
||
|
memoryAvailable, exists := pairs[memoryAvailableKey]
|
||
|
if !exists {
|
||
|
return nil, fmt.Errorf("parsing memory information: %w, key=%s", ErrKeyNotFound, memoryAvailableKey)
|
||
|
}
|
||
|
|
||
|
memoryCurrent := memoryMaximum - memoryAvailable
|
||
|
|
||
|
return &MemoryInformation{memoryMaximum, memoryCurrent}, nil
|
||
|
}
|
||
|
|
||
|
func RawSystemInformation(osInfoRaw string, memoryInfoRaw string, fdInfoRaw string, disksRaw string) string {
|
||
|
var builder strings.Builder
|
||
|
|
||
|
formatInfo := func(info string, builder *strings.Builder) {
|
||
|
if info == "" {
|
||
|
builder.WriteString("No information\n")
|
||
|
} else {
|
||
|
builder.WriteString(info)
|
||
|
builder.WriteString("\n")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
builder.WriteString("---BEGIN Operating system information\n")
|
||
|
formatInfo(osInfoRaw, &builder)
|
||
|
builder.WriteString("---END Operating system information\n")
|
||
|
builder.WriteString("---BEGIN Memory information\n")
|
||
|
formatInfo(memoryInfoRaw, &builder)
|
||
|
builder.WriteString("---END Memory information\n")
|
||
|
builder.WriteString("---BEGIN File descriptors information\n")
|
||
|
formatInfo(fdInfoRaw, &builder)
|
||
|
builder.WriteString("---END File descriptors information\n")
|
||
|
builder.WriteString("---BEGIN Disks information\n")
|
||
|
formatInfo(disksRaw, &builder)
|
||
|
builder.WriteString("---END Disks information\n")
|
||
|
|
||
|
rawInformation := builder.String()
|
||
|
|
||
|
return rawInformation
|
||
|
}
|
||
|
|
||
|
func collectDiskVolumeInformationUnix(ctx context.Context) ([]*DiskVolumeInformation, string, error) {
|
||
|
command := exec.CommandContext(ctx, "df", "-k")
|
||
|
|
||
|
stdout, err := command.Output()
|
||
|
if err != nil {
|
||
|
return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err)
|
||
|
}
|
||
|
|
||
|
output := string(stdout)
|
||
|
|
||
|
disks, err := ParseDiskVolumeInformationOutput(output, 1, 1)
|
||
|
if err != nil {
|
||
|
return nil, output, err
|
||
|
}
|
||
|
|
||
|
// returning raw output in case other collected information
|
||
|
// resulted in errors
|
||
|
return disks, output, nil
|
||
|
}
|
||
|
|
||
|
func collectOSInformationUnix(ctx context.Context) (*OsInfo, string, error) {
|
||
|
command := exec.CommandContext(ctx, "uname", "-a")
|
||
|
|
||
|
stdout, err := command.Output()
|
||
|
if err != nil {
|
||
|
return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err)
|
||
|
}
|
||
|
|
||
|
output := string(stdout)
|
||
|
|
||
|
osInfo, err := ParseUnameOutput(output, runtime.GOOS)
|
||
|
if err != nil {
|
||
|
return nil, output, err
|
||
|
}
|
||
|
|
||
|
// returning raw output in case other collected information
|
||
|
// resulted in errors
|
||
|
return osInfo, output, nil
|
||
|
}
|