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 }