TUN-8731: Implement diag/system endpoint
## Summary This PR will add a new endpoint, "diag/system" to the metrics server that collects system information from different operating systems. Closes TUN-8731
This commit is contained in:
parent
e2c2b012f1
commit
aab5364252
|
@ -28,6 +28,7 @@ import (
|
|||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/credentials"
|
||||
"github.com/cloudflare/cloudflared/diagnostic"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/features"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
|
@ -463,8 +464,10 @@ func StartServer(
|
|||
readinessServer := metrics.NewReadyServer(clientID,
|
||||
tunnelstate.NewConnTracker(log))
|
||||
observer.RegisterSink(readinessServer)
|
||||
diagnosticHandler := diagnostic.NewDiagnosticHandler(log, 0, diagnostic.NewSystemCollectorImpl(buildInfo.CloudflaredVersion))
|
||||
metricsConfig := metrics.Config{
|
||||
ReadyServer: readinessServer,
|
||||
DiagnosticHandler: diagnosticHandler,
|
||||
QuickTunnelHostname: quickTunnelURL,
|
||||
Orchestrator: orchestrator,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package diagnostic
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
defaultCollectorTimeout = time.Second * 10 // This const define the timeout value of a collector operation.
|
||||
collectorField = "collector" // used for logging purposes
|
||||
systemCollectorName = "system" // used for logging purposes
|
||||
)
|
|
@ -0,0 +1,16 @@
|
|||
package diagnostic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// Error used when parsing the fields of the output of collector.
|
||||
ErrInsufficientLines = errors.New("insufficient lines")
|
||||
// Error used when parsing the lines of the output of collector.
|
||||
ErrInsuficientFields = errors.New("insufficient fields")
|
||||
// Error used when given key is not found while parsing KV.
|
||||
ErrKeyNotFound = errors.New("key not found")
|
||||
// Error used when tehre is no disk volume information available
|
||||
ErrNoVolumeFound = errors.New("No disk volume information found")
|
||||
)
|
|
@ -0,0 +1,83 @@
|
|||
package diagnostic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
log *zerolog.Logger
|
||||
timeout time.Duration
|
||||
systemCollector SystemCollector
|
||||
}
|
||||
|
||||
func NewDiagnosticHandler(
|
||||
log *zerolog.Logger,
|
||||
timeout time.Duration,
|
||||
systemCollector SystemCollector,
|
||||
) *Handler {
|
||||
if timeout == 0 {
|
||||
timeout = defaultCollectorTimeout
|
||||
}
|
||||
|
||||
return &Handler{
|
||||
log,
|
||||
timeout,
|
||||
systemCollector,
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *Handler) SystemHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
logger := handler.log.With().Str(collectorField, systemCollectorName).Logger()
|
||||
logger.Info().Msg("Collection started")
|
||||
|
||||
defer func() {
|
||||
logger.Info().Msg("Collection finished")
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(request.Context(), handler.timeout)
|
||||
|
||||
defer cancel()
|
||||
|
||||
info, rawInfo, err := handler.systemCollector.Collect(ctx)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("error occurred whilst collecting system information")
|
||||
|
||||
if rawInfo != "" {
|
||||
logger.Info().Msg("using raw information fallback")
|
||||
bytes := []byte(rawInfo)
|
||||
writeResponse(writer, bytes, &logger)
|
||||
} else {
|
||||
logger.Error().Msg("no raw information available")
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if info == nil {
|
||||
logger.Error().Msgf("system information collection is nil")
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(writer)
|
||||
|
||||
err = encoder.Encode(info)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("error occurred whilst serializing information")
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func writeResponse(writer http.ResponseWriter, bytes []byte, logger *zerolog.Logger) {
|
||||
bytesWritten, err := writer.Write(bytes)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("error occurred writing response")
|
||||
} else if bytesWritten != len(bytes) {
|
||||
logger.Error().Msgf("error incomplete write response %d/%d", bytesWritten, len(bytes))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
package diagnostic_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/diagnostic"
|
||||
)
|
||||
|
||||
type SystemCollectorMock struct{}
|
||||
|
||||
const (
|
||||
systemInformationKey = "sikey"
|
||||
rawInformationKey = "rikey"
|
||||
errorKey = "errkey"
|
||||
)
|
||||
|
||||
func setCtxValuesForSystemCollector(
|
||||
systemInfo *diagnostic.SystemInformation,
|
||||
rawInfo string,
|
||||
err error,
|
||||
) context.Context {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, systemInformationKey, systemInfo)
|
||||
ctx = context.WithValue(ctx, rawInformationKey, rawInfo)
|
||||
ctx = context.WithValue(ctx, errorKey, err)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (*SystemCollectorMock) Collect(ctx context.Context) (*diagnostic.SystemInformation, string, error) {
|
||||
si, _ := ctx.Value(systemInformationKey).(*diagnostic.SystemInformation)
|
||||
ri, _ := ctx.Value(rawInformationKey).(string)
|
||||
err, _ := ctx.Value(errorKey).(error)
|
||||
|
||||
return si, ri, err
|
||||
}
|
||||
|
||||
func TestSystemHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
log := zerolog.Nop()
|
||||
tests := []struct {
|
||||
name string
|
||||
systemInfo *diagnostic.SystemInformation
|
||||
rawInfo string
|
||||
err error
|
||||
statusCode int
|
||||
}{
|
||||
{
|
||||
name: "happy path",
|
||||
systemInfo: diagnostic.NewSystemInformation(
|
||||
0, 0, 0, 0,
|
||||
"string", "string", "string", "string",
|
||||
"string", "string", nil,
|
||||
),
|
||||
rawInfo: "",
|
||||
err: nil,
|
||||
statusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "on error and raw info", systemInfo: nil,
|
||||
rawInfo: "raw info", err: errors.New("an error"), statusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "on error and no raw info", systemInfo: nil,
|
||||
rawInfo: "", err: errors.New("an error"), statusCode: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "malformed response", systemInfo: nil, rawInfo: "", err: nil, statusCode: http.StatusInternalServerError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tCase := range tests {
|
||||
t.Run(tCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
handler := diagnostic.NewDiagnosticHandler(&log, 0, &SystemCollectorMock{})
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx := setCtxValuesForSystemCollector(tCase.systemInfo, tCase.rawInfo, tCase.err)
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, "/diag/syste,", nil)
|
||||
require.NoError(t, err)
|
||||
handler.SystemHandler(recorder, request)
|
||||
|
||||
assert.Equal(t, tCase.statusCode, recorder.Code)
|
||||
if tCase.statusCode == http.StatusOK && tCase.systemInfo != nil {
|
||||
var response diagnostic.SystemInformation
|
||||
|
||||
decoder := json.NewDecoder(recorder.Body)
|
||||
err = decoder.Decode(&response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tCase.systemInfo, &response)
|
||||
} else if tCase.statusCode == http.StatusOK && tCase.rawInfo != "" {
|
||||
rawBytes, err := io.ReadAll(recorder.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tCase.rawInfo, string(rawBytes))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package diagnostic
|
||||
|
||||
import "context"
|
||||
|
||||
type DiskVolumeInformation struct {
|
||||
Name string `json:"name"` // represents the filesystem in linux/macos or device name in windows
|
||||
SizeMaximum uint64 `json:"sizeMaximum"` // represents the maximum size of the disk in kilobytes
|
||||
SizeCurrent uint64 `json:"sizeCurrent"` // represents the current size of the disk in kilobytes
|
||||
}
|
||||
|
||||
func NewDiskVolumeInformation(name string, maximum, current uint64) *DiskVolumeInformation {
|
||||
return &DiskVolumeInformation{
|
||||
name,
|
||||
maximum,
|
||||
current,
|
||||
}
|
||||
}
|
||||
|
||||
type SystemInformation struct {
|
||||
MemoryMaximum uint64 `json:"memoryMaximum"` // represents the maximum memory of the system in kilobytes
|
||||
MemoryCurrent uint64 `json:"memoryCurrent"` // represents the system's memory in use in kilobytes
|
||||
FileDescriptorMaximum uint64 `json:"fileDescriptorMaximum"` // represents the maximum number of file descriptors of the system
|
||||
FileDescriptorCurrent uint64 `json:"fileDescriptorCurrent"` // represents the system's file descriptors in use
|
||||
OsSystem string `json:"osSystem"` // represents the operating system name i.e.: linux, windows, darwin
|
||||
HostName string `json:"hostName"` // represents the system host name
|
||||
OsVersion string `json:"osVersion"` // detailed information about the system's release version level
|
||||
OsRelease string `json:"osRelease"` // detailed information about the system's release
|
||||
Architecture string `json:"architecture"` // represents the system's hardware platform i.e: arm64/amd64
|
||||
CloudflaredVersion string `json:"cloudflaredVersion"` // the runtime version of cloudflared
|
||||
Disk []*DiskVolumeInformation `json:"disk"`
|
||||
}
|
||||
|
||||
func NewSystemInformation(
|
||||
memoryMaximum,
|
||||
memoryCurrent,
|
||||
filesMaximum,
|
||||
filesCurrent uint64,
|
||||
osystem,
|
||||
name,
|
||||
osVersion,
|
||||
osRelease,
|
||||
architecture,
|
||||
cloudflaredVersion string,
|
||||
disk []*DiskVolumeInformation,
|
||||
) *SystemInformation {
|
||||
return &SystemInformation{
|
||||
memoryMaximum,
|
||||
memoryCurrent,
|
||||
filesMaximum,
|
||||
filesCurrent,
|
||||
osystem,
|
||||
name,
|
||||
osVersion,
|
||||
osRelease,
|
||||
architecture,
|
||||
cloudflaredVersion,
|
||||
disk,
|
||||
}
|
||||
}
|
||||
|
||||
type SystemCollector interface {
|
||||
// If the collection is successful it will return `SystemInformation` struct,
|
||||
// an empty string, and a nil error.
|
||||
// In case there is an error a string with the raw data will be returned
|
||||
// however the returned string not contain all the data points.
|
||||
//
|
||||
// This function expects that the caller sets the context timeout to prevent
|
||||
// long-lived collectors.
|
||||
Collect(ctx context.Context) (*SystemInformation, string, error)
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
//go:build linux
|
||||
|
||||
package diagnostic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SystemCollectorImpl struct {
|
||||
version string
|
||||
}
|
||||
|
||||
func NewSystemCollectorImpl(
|
||||
version string,
|
||||
) *SystemCollectorImpl {
|
||||
return &SystemCollectorImpl{
|
||||
version,
|
||||
}
|
||||
}
|
||||
|
||||
func (collector *SystemCollectorImpl) Collect(ctx context.Context) (*SystemInformation, string, error) {
|
||||
memoryInfo, memoryInfoRaw, memoryInfoErr := collectMemoryInformation(ctx)
|
||||
fdInfo, fdInfoRaw, fdInfoErr := collectFileDescriptorInformation(ctx)
|
||||
disks, disksRaw, diskErr := collectDiskVolumeInformationUnix(ctx)
|
||||
osInfo, osInfoRaw, osInfoErr := collectOSInformationUnix(ctx)
|
||||
|
||||
if memoryInfoErr != nil {
|
||||
raw := RawSystemInformation(osInfoRaw, memoryInfoRaw, fdInfoRaw, disksRaw)
|
||||
return nil, raw, memoryInfoErr
|
||||
}
|
||||
|
||||
if fdInfoErr != nil {
|
||||
raw := RawSystemInformation(osInfoRaw, memoryInfoRaw, fdInfoRaw, disksRaw)
|
||||
return nil, raw, fdInfoErr
|
||||
}
|
||||
|
||||
if diskErr != nil {
|
||||
raw := RawSystemInformation(osInfoRaw, memoryInfoRaw, fdInfoRaw, disksRaw)
|
||||
return nil, raw, diskErr
|
||||
}
|
||||
|
||||
if osInfoErr != nil {
|
||||
raw := RawSystemInformation(osInfoRaw, memoryInfoRaw, fdInfoRaw, disksRaw)
|
||||
return nil, raw, osInfoErr
|
||||
}
|
||||
|
||||
return NewSystemInformation(
|
||||
memoryInfo.MemoryMaximum,
|
||||
memoryInfo.MemoryCurrent,
|
||||
fdInfo.FileDescriptorMaximum,
|
||||
fdInfo.FileDescriptorCurrent,
|
||||
osInfo.OsSystem,
|
||||
osInfo.Name,
|
||||
osInfo.OsVersion,
|
||||
osInfo.OsRelease,
|
||||
osInfo.Architecture,
|
||||
collector.version,
|
||||
disks,
|
||||
), "", nil
|
||||
}
|
||||
|
||||
func collectMemoryInformation(ctx context.Context) (*MemoryInformation, string, error) {
|
||||
// This function relies on the output of `cat /proc/meminfo` to retrieve
|
||||
// memoryMax and memoryCurrent.
|
||||
// The expected output is in the format of `KEY VALUE UNIT`.
|
||||
const (
|
||||
memTotalPrefix = "MemTotal"
|
||||
memAvailablePrefix = "MemAvailable"
|
||||
)
|
||||
|
||||
command := exec.CommandContext(ctx, "cat", "/proc/meminfo")
|
||||
|
||||
stdout, err := command.Output()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err)
|
||||
}
|
||||
|
||||
output := string(stdout)
|
||||
|
||||
mapper := func(field string) (uint64, error) {
|
||||
field = strings.TrimRight(field, " kB")
|
||||
|
||||
return strconv.ParseUint(field, 10, 64)
|
||||
}
|
||||
|
||||
memoryInfo, err := ParseMemoryInformationFromKV(output, memTotalPrefix, memAvailablePrefix, mapper)
|
||||
if err != nil {
|
||||
return nil, output, err
|
||||
}
|
||||
|
||||
// returning raw output in case other collected information
|
||||
// resulted in errors
|
||||
return memoryInfo, output, nil
|
||||
}
|
||||
|
||||
func collectFileDescriptorInformation(ctx context.Context) (*FileDescriptorInformation, string, error) {
|
||||
// Command retrieved from https://docs.kernel.org/admin-guide/sysctl/fs.html#file-max-file-nr.
|
||||
// If the sysctl is not available the command with fail.
|
||||
command := exec.CommandContext(ctx, "sysctl", "-n", "fs.file-nr")
|
||||
|
||||
stdout, err := command.Output()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err)
|
||||
}
|
||||
|
||||
output := string(stdout)
|
||||
|
||||
fileDescriptorInfo, err := ParseSysctlFileDescriptorInformation(output)
|
||||
if err != nil {
|
||||
return nil, output, err
|
||||
}
|
||||
|
||||
// returning raw output in case other collected information
|
||||
// resulted in errors
|
||||
return fileDescriptorInfo, output, nil
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
//go:build darwin
|
||||
|
||||
package diagnostic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type SystemCollectorImpl struct {
|
||||
version string
|
||||
}
|
||||
|
||||
func NewSystemCollectorImpl(
|
||||
version string,
|
||||
) *SystemCollectorImpl {
|
||||
return &SystemCollectorImpl{
|
||||
version,
|
||||
}
|
||||
}
|
||||
|
||||
func (collector *SystemCollectorImpl) Collect(ctx context.Context) (*SystemInformation, string, error) {
|
||||
memoryInfo, memoryInfoRaw, memoryInfoErr := collectMemoryInformation(ctx)
|
||||
fdInfo, fdInfoRaw, fdInfoErr := collectFileDescriptorInformation(ctx)
|
||||
disks, disksRaw, diskErr := collectDiskVolumeInformationUnix(ctx)
|
||||
osInfo, osInfoRaw, osInfoErr := collectOSInformationUnix(ctx)
|
||||
|
||||
if memoryInfoErr != nil {
|
||||
return nil, RawSystemInformation(osInfoRaw, memoryInfoRaw, fdInfoRaw, disksRaw), memoryInfoErr
|
||||
}
|
||||
|
||||
if fdInfoErr != nil {
|
||||
return nil, RawSystemInformation(osInfoRaw, memoryInfoRaw, fdInfoRaw, disksRaw), fdInfoErr
|
||||
}
|
||||
|
||||
if diskErr != nil {
|
||||
return nil, RawSystemInformation(osInfoRaw, memoryInfoRaw, fdInfoRaw, disksRaw), diskErr
|
||||
}
|
||||
|
||||
if osInfoErr != nil {
|
||||
return nil, RawSystemInformation(osInfoRaw, memoryInfoRaw, fdInfoRaw, disksRaw), osInfoErr
|
||||
}
|
||||
|
||||
return NewSystemInformation(
|
||||
memoryInfo.MemoryMaximum,
|
||||
memoryInfo.MemoryCurrent,
|
||||
fdInfo.FileDescriptorMaximum,
|
||||
fdInfo.FileDescriptorCurrent,
|
||||
osInfo.OsSystem,
|
||||
osInfo.Name,
|
||||
osInfo.OsVersion,
|
||||
osInfo.OsRelease,
|
||||
osInfo.Architecture,
|
||||
collector.version,
|
||||
disks,
|
||||
), "", nil
|
||||
}
|
||||
|
||||
func collectFileDescriptorInformation(ctx context.Context) (
|
||||
*FileDescriptorInformation,
|
||||
string,
|
||||
error,
|
||||
) {
|
||||
const (
|
||||
fileDescriptorMaximumKey = "kern.maxfiles"
|
||||
fileDescriptorCurrentKey = "kern.num_files"
|
||||
)
|
||||
|
||||
command := exec.CommandContext(ctx, "sysctl", fileDescriptorMaximumKey, fileDescriptorCurrentKey)
|
||||
|
||||
stdout, err := command.Output()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err)
|
||||
}
|
||||
|
||||
output := string(stdout)
|
||||
|
||||
fileDescriptorInfo, err := ParseFileDescriptorInformationFromKV(
|
||||
output,
|
||||
fileDescriptorMaximumKey,
|
||||
fileDescriptorCurrentKey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, output, err
|
||||
}
|
||||
|
||||
// returning raw output in case other collected information
|
||||
// resulted in errors
|
||||
return fileDescriptorInfo, output, nil
|
||||
}
|
||||
|
||||
func collectMemoryInformation(ctx context.Context) (
|
||||
*MemoryInformation,
|
||||
string,
|
||||
error,
|
||||
) {
|
||||
const (
|
||||
memoryMaximumKey = "hw.memsize"
|
||||
memoryAvailableKey = "hw.memsize_usable"
|
||||
)
|
||||
|
||||
command := exec.CommandContext(
|
||||
ctx,
|
||||
"sysctl",
|
||||
memoryMaximumKey,
|
||||
memoryAvailableKey,
|
||||
)
|
||||
|
||||
stdout, err := command.Output()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err)
|
||||
}
|
||||
|
||||
output := string(stdout)
|
||||
|
||||
mapper := func(field string) (uint64, error) {
|
||||
const kiloBytes = 1024
|
||||
value, err := strconv.ParseUint(field, 10, 64)
|
||||
return value / kiloBytes, err
|
||||
}
|
||||
|
||||
memoryInfo, err := ParseMemoryInformationFromKV(output, memoryMaximumKey, memoryAvailableKey, mapper)
|
||||
if err != nil {
|
||||
return nil, output, err
|
||||
}
|
||||
|
||||
// returning raw output in case other collected information
|
||||
// resulted in errors
|
||||
return memoryInfo, output, nil
|
||||
}
|
|
@ -0,0 +1,466 @@
|
|||
package diagnostic_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/diagnostic"
|
||||
)
|
||||
|
||||
func TestParseMemoryInformationFromKV(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mapper := func(field string) (uint64, error) {
|
||||
value, err := strconv.ParseUint(field, 10, 64)
|
||||
return value, err
|
||||
}
|
||||
|
||||
linuxMapper := func(field string) (uint64, error) {
|
||||
field = strings.TrimRight(field, " kB")
|
||||
return strconv.ParseUint(field, 10, 64)
|
||||
}
|
||||
|
||||
windowsMemoryOutput := `
|
||||
|
||||
FreeVirtualMemory : 5350472
|
||||
TotalVirtualMemorySize : 8903424
|
||||
|
||||
|
||||
`
|
||||
macosMemoryOutput := `hw.memsize: 38654705664
|
||||
hw.memsize_usable: 38009012224`
|
||||
memoryOutputWithMissingKey := `hw.memsize: 38654705664`
|
||||
|
||||
linuxMemoryOutput := `MemTotal: 8028860 kB
|
||||
MemFree: 731396 kB
|
||||
MemAvailable: 4678844 kB
|
||||
Buffers: 472632 kB
|
||||
Cached: 3186492 kB
|
||||
SwapCached: 4196 kB
|
||||
Active: 3088988 kB
|
||||
Inactive: 3468560 kB`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
output string
|
||||
memoryMaximumKey string
|
||||
memoryAvailableKey string
|
||||
expected *diagnostic.MemoryInformation
|
||||
expectedErr bool
|
||||
mapper func(string) (uint64, error)
|
||||
}{
|
||||
{
|
||||
name: "parse linux memory values",
|
||||
output: linuxMemoryOutput,
|
||||
memoryMaximumKey: "MemTotal",
|
||||
memoryAvailableKey: "MemAvailable",
|
||||
expected: &diagnostic.MemoryInformation{
|
||||
8028860,
|
||||
8028860 - 4678844,
|
||||
},
|
||||
expectedErr: false,
|
||||
mapper: linuxMapper,
|
||||
},
|
||||
{
|
||||
name: "parse memory values with missing key",
|
||||
output: memoryOutputWithMissingKey,
|
||||
memoryMaximumKey: "hw.memsize",
|
||||
memoryAvailableKey: "hw.memsize_usable",
|
||||
expected: nil,
|
||||
expectedErr: true,
|
||||
mapper: mapper,
|
||||
},
|
||||
{
|
||||
name: "parse macos memory values",
|
||||
output: macosMemoryOutput,
|
||||
memoryMaximumKey: "hw.memsize",
|
||||
memoryAvailableKey: "hw.memsize_usable",
|
||||
expected: &diagnostic.MemoryInformation{
|
||||
38654705664,
|
||||
38654705664 - 38009012224,
|
||||
},
|
||||
expectedErr: false,
|
||||
mapper: mapper,
|
||||
},
|
||||
{
|
||||
name: "parse windows memory values",
|
||||
output: windowsMemoryOutput,
|
||||
memoryMaximumKey: "TotalVirtualMemorySize",
|
||||
memoryAvailableKey: "FreeVirtualMemory",
|
||||
expected: &diagnostic.MemoryInformation{
|
||||
8903424,
|
||||
8903424 - 5350472,
|
||||
},
|
||||
expectedErr: false,
|
||||
mapper: mapper,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tCase := range tests {
|
||||
t.Run(tCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
memoryInfo, err := diagnostic.ParseMemoryInformationFromKV(
|
||||
tCase.output,
|
||||
tCase.memoryMaximumKey,
|
||||
tCase.memoryAvailableKey,
|
||||
tCase.mapper,
|
||||
)
|
||||
|
||||
if tCase.expectedErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tCase.expected, memoryInfo)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUnameOutput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
output string
|
||||
os string
|
||||
expected *diagnostic.OsInfo
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "darwin machine",
|
||||
output: "Darwin APC 23.6.0 Darwin Kernel Version 99.6.0: Wed Jul 31 20:48:04 PDT 1997; root:xnu-66666.666.6.666.6~1/RELEASE_ARM64_T6666 arm64",
|
||||
os: "darwin",
|
||||
expected: &diagnostic.OsInfo{
|
||||
Architecture: "arm64",
|
||||
Name: "APC",
|
||||
OsSystem: "Darwin",
|
||||
OsRelease: "Darwin Kernel Version 99.6.0: Wed Jul 31 20:48:04 PDT 1997; root:xnu-66666.666.6.666.6~1/RELEASE_ARM64_T6666",
|
||||
OsVersion: "23.6.0",
|
||||
},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "linux machine",
|
||||
output: "Linux dab00d565591 6.6.31-linuxkit #1 SMP Thu May 23 08:36:57 UTC 2024 aarch64 GNU/Linux",
|
||||
os: "linux",
|
||||
expected: &diagnostic.OsInfo{
|
||||
Architecture: "aarch64",
|
||||
Name: "dab00d565591",
|
||||
OsSystem: "Linux",
|
||||
OsRelease: "#1 SMP Thu May 23 08:36:57 UTC 2024",
|
||||
OsVersion: "6.6.31-linuxkit",
|
||||
},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "not enough fields",
|
||||
output: "Linux ",
|
||||
os: "linux",
|
||||
expected: nil,
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tCase := range tests {
|
||||
t.Run(tCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
memoryInfo, err := diagnostic.ParseUnameOutput(
|
||||
tCase.output,
|
||||
tCase.os,
|
||||
)
|
||||
|
||||
if tCase.expectedErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tCase.expected, memoryInfo)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFileDescriptorInformationFromKV(t *testing.T) {
|
||||
const (
|
||||
fileDescriptorMaximumKey = "kern.maxfiles"
|
||||
fileDescriptorCurrentKey = "kern.num_files"
|
||||
)
|
||||
|
||||
t.Parallel()
|
||||
|
||||
memoryOutput := `kern.maxfiles: 276480
|
||||
kern.num_files: 11787`
|
||||
memoryOutputWithMissingKey := `kern.maxfiles: 276480`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
output string
|
||||
expected *diagnostic.FileDescriptorInformation
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "parse memory values with missing key",
|
||||
output: memoryOutputWithMissingKey,
|
||||
expected: nil,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "parse macos memory values",
|
||||
output: memoryOutput,
|
||||
expected: &diagnostic.FileDescriptorInformation{
|
||||
276480,
|
||||
11787,
|
||||
},
|
||||
expectedErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tCase := range tests {
|
||||
t.Run(tCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fdInfo, err := diagnostic.ParseFileDescriptorInformationFromKV(
|
||||
tCase.output,
|
||||
fileDescriptorMaximumKey,
|
||||
fileDescriptorCurrentKey,
|
||||
)
|
||||
|
||||
if tCase.expectedErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tCase.expected, fdInfo)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSysctlFileDescriptorInformation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
output string
|
||||
expected *diagnostic.FileDescriptorInformation
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "expected output",
|
||||
output: "111 0 1111111",
|
||||
expected: &diagnostic.FileDescriptorInformation{
|
||||
FileDescriptorMaximum: 1111111,
|
||||
FileDescriptorCurrent: 111,
|
||||
},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "not enough fields",
|
||||
output: "111 111 ",
|
||||
expected: nil,
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tCase := range tests {
|
||||
t.Run(tCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
fdsInfo, err := diagnostic.ParseSysctlFileDescriptorInformation(
|
||||
tCase.output,
|
||||
)
|
||||
|
||||
if tCase.expectedErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tCase.expected, fdsInfo)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseWinOperatingSystemInfo(t *testing.T) {
|
||||
const (
|
||||
architecturePrefix = "OSArchitecture"
|
||||
osSystemPrefix = "Caption"
|
||||
osVersionPrefix = "Version"
|
||||
osReleasePrefix = "BuildNumber"
|
||||
namePrefix = "CSName"
|
||||
)
|
||||
|
||||
t.Parallel()
|
||||
|
||||
windowsIncompleteOsInfo := `
|
||||
OSArchitecture : ARM 64 bits
|
||||
Caption : Microsoft Windows 11 Home
|
||||
Morekeys : 121314
|
||||
CSName : UTILIZA-QO859QP
|
||||
`
|
||||
windowsCompleteOsInfo := `
|
||||
OSArchitecture : ARM 64 bits
|
||||
Caption : Microsoft Windows 11 Home
|
||||
Version : 10.0.22631
|
||||
BuildNumber : 22631
|
||||
Morekeys : 121314
|
||||
CSName : UTILIZA-QO859QP
|
||||
`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
output string
|
||||
expected *diagnostic.OsInfo
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "expected output",
|
||||
output: windowsCompleteOsInfo,
|
||||
expected: &diagnostic.OsInfo{
|
||||
Architecture: "ARM 64 bits",
|
||||
Name: "UTILIZA-QO859QP",
|
||||
OsSystem: "Microsoft Windows 11 Home",
|
||||
OsRelease: "22631",
|
||||
OsVersion: "10.0.22631",
|
||||
},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing keys",
|
||||
output: windowsIncompleteOsInfo,
|
||||
expected: nil,
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tCase := range tests {
|
||||
t.Run(tCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
osInfo, err := diagnostic.ParseWinOperatingSystemInfo(
|
||||
tCase.output,
|
||||
architecturePrefix,
|
||||
osSystemPrefix,
|
||||
osVersionPrefix,
|
||||
osReleasePrefix,
|
||||
namePrefix,
|
||||
)
|
||||
|
||||
if tCase.expectedErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tCase.expected, osInfo)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDiskVolumeInformationOutput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
invalidUnixDiskVolumeInfo := `Filesystem Size Used Avail Use% Mounted on
|
||||
overlay 59G 19G 38G 33% /
|
||||
tmpfs 64M 0 64M 0% /dev
|
||||
shm 64M 0 64M 0% /dev/shm
|
||||
/run/host_mark/Users 461G 266G 195G 58% /tmp/cloudflared
|
||||
/dev/vda1 59G 19G 38G 33% /etc/hosts
|
||||
tmpfs 3.9G 0 3.9G 0% /sys/firmware
|
||||
`
|
||||
|
||||
unixDiskVolumeInfo := `Filesystem Size Used Avail Use% Mounted on
|
||||
overlay 61202244 18881444 39179476 33% /
|
||||
tmpfs 65536 0 65536 0% /dev
|
||||
shm 65536 0 65536 0% /dev/shm
|
||||
/run/host_mark/Users 482797652 278648468 204149184 58% /tmp/cloudflared
|
||||
/dev/vda1 61202244 18881444 39179476 33% /etc/hosts
|
||||
tmpfs 4014428 0 4014428 0% /sys/firmware`
|
||||
missingFields := ` DeviceID Size
|
||||
-------- ----
|
||||
C: size
|
||||
E: 235563008
|
||||
Z: 67754782720
|
||||
`
|
||||
invalidTypeField := ` DeviceID Size FreeSpace
|
||||
-------- ---- ---------
|
||||
C: size 31318736896
|
||||
D:
|
||||
E: 235563008 0
|
||||
Z: 67754782720 31318732800
|
||||
`
|
||||
|
||||
windowsDiskVolumeInfo := `
|
||||
|
||||
DeviceID Size FreeSpace
|
||||
-------- ---- ---------
|
||||
C: 67754782720 31318736896
|
||||
E: 235563008 0
|
||||
Z: 67754782720 31318732800`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
output string
|
||||
expected []*diagnostic.DiskVolumeInformation
|
||||
skipLines int
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "invalid unix disk volume information (numbers have units)",
|
||||
output: invalidUnixDiskVolumeInfo,
|
||||
expected: []*diagnostic.DiskVolumeInformation{},
|
||||
skipLines: 1,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "unix disk volume information",
|
||||
output: unixDiskVolumeInfo,
|
||||
skipLines: 1,
|
||||
expected: []*diagnostic.DiskVolumeInformation{
|
||||
diagnostic.NewDiskVolumeInformation("overlay", 61202244, 18881444),
|
||||
diagnostic.NewDiskVolumeInformation("tmpfs", 65536, 0),
|
||||
diagnostic.NewDiskVolumeInformation("shm", 65536, 0),
|
||||
diagnostic.NewDiskVolumeInformation("/run/host_mark/Users", 482797652, 278648468),
|
||||
diagnostic.NewDiskVolumeInformation("/dev/vda1", 61202244, 18881444),
|
||||
diagnostic.NewDiskVolumeInformation("tmpfs", 4014428, 0),
|
||||
},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "windows disk volume information",
|
||||
output: windowsDiskVolumeInfo,
|
||||
expected: []*diagnostic.DiskVolumeInformation{
|
||||
diagnostic.NewDiskVolumeInformation("C:", 67754782720, 31318736896),
|
||||
diagnostic.NewDiskVolumeInformation("E:", 235563008, 0),
|
||||
diagnostic.NewDiskVolumeInformation("Z:", 67754782720, 31318732800),
|
||||
},
|
||||
skipLines: 4,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "insuficient fields",
|
||||
output: missingFields,
|
||||
expected: nil,
|
||||
skipLines: 2,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid field",
|
||||
output: invalidTypeField,
|
||||
expected: nil,
|
||||
skipLines: 2,
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tCase := range tests {
|
||||
t.Run(tCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
disks, err := diagnostic.ParseDiskVolumeInformationOutput(tCase.output, tCase.skipLines, 1)
|
||||
|
||||
if tCase.expectedErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tCase.expected, disks)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,377 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
//go:build windows
|
||||
|
||||
package diagnostic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const kiloBytesScale = 1.0 / 1024
|
||||
|
||||
type SystemCollectorImpl struct {
|
||||
version string
|
||||
}
|
||||
|
||||
func NewSystemCollectorImpl(
|
||||
version string,
|
||||
) *SystemCollectorImpl {
|
||||
return &SystemCollectorImpl{
|
||||
version,
|
||||
}
|
||||
}
|
||||
func (collector *SystemCollectorImpl) Collect(ctx context.Context) (*SystemInformation, string, error) {
|
||||
memoryInfo, memoryInfoRaw, memoryInfoErr := collectMemoryInformation(ctx)
|
||||
disks, disksRaw, diskErr := collectDiskVolumeInformation(ctx)
|
||||
osInfo, osInfoRaw, osInfoErr := collectOSInformation(ctx)
|
||||
|
||||
if memoryInfoErr != nil {
|
||||
raw := RawSystemInformation(osInfoRaw, memoryInfoRaw, "", disksRaw)
|
||||
return nil, raw, memoryInfoErr
|
||||
}
|
||||
|
||||
if diskErr != nil {
|
||||
raw := RawSystemInformation(osInfoRaw, memoryInfoRaw, "", disksRaw)
|
||||
return nil, raw, diskErr
|
||||
}
|
||||
|
||||
if osInfoErr != nil {
|
||||
raw := RawSystemInformation(osInfoRaw, memoryInfoRaw, "", disksRaw)
|
||||
return nil, raw, osInfoErr
|
||||
}
|
||||
|
||||
return NewSystemInformation(
|
||||
memoryInfo.MemoryMaximum,
|
||||
memoryInfo.MemoryCurrent,
|
||||
// For windows we leave both the fileDescriptorMaximum and fileDescriptorCurrent with zero
|
||||
// since there is no obvious way to get this information.
|
||||
0,
|
||||
0,
|
||||
osInfo.OsSystem,
|
||||
osInfo.Name,
|
||||
osInfo.OsVersion,
|
||||
osInfo.OsRelease,
|
||||
osInfo.Architecture,
|
||||
collector.version,
|
||||
disks,
|
||||
), "", nil
|
||||
}
|
||||
|
||||
func collectMemoryInformation(ctx context.Context) (*MemoryInformation, string, error) {
|
||||
const (
|
||||
memoryTotalPrefix = "TotalVirtualMemorySize"
|
||||
memoryAvailablePrefix = "FreeVirtualMemory"
|
||||
)
|
||||
|
||||
command := exec.CommandContext(
|
||||
ctx,
|
||||
"powershell",
|
||||
"-Command",
|
||||
"Get-CimInstance -Class Win32_OperatingSystem | Select-Object FreeVirtualMemory, TotalVirtualMemorySize | Format-List",
|
||||
)
|
||||
|
||||
stdout, err := command.Output()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error retrieving output from command '%s': %w", command.String(), err)
|
||||
}
|
||||
|
||||
output := string(stdout)
|
||||
|
||||
// the result of the command above will return values in bytes hence
|
||||
// they need to be converted to kilobytes
|
||||
mapper := func(field string) (uint64, error) {
|
||||
value, err := strconv.ParseUint(field, 10, 64)
|
||||
return uint64(float64(value) * kiloBytesScale), err
|
||||
}
|
||||
|
||||
memoryInfo, err := ParseMemoryInformationFromKV(output, memoryTotalPrefix, memoryAvailablePrefix, mapper)
|
||||
if err != nil {
|
||||
return nil, output, err
|
||||
}
|
||||
|
||||
// returning raw output in case other collected information
|
||||
// resulted in errors
|
||||
return memoryInfo, output, nil
|
||||
}
|
||||
|
||||
func collectDiskVolumeInformation(ctx context.Context) ([]*DiskVolumeInformation, string, error) {
|
||||
|
||||
command := exec.CommandContext(
|
||||
ctx,
|
||||
"powershell", "-Command", "Get-CimInstance -Class Win32_LogicalDisk | Select-Object DeviceID, Size, FreeSpace")
|
||||
|
||||
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, 2, kiloBytesScale)
|
||||
if err != nil {
|
||||
return nil, output, err
|
||||
}
|
||||
|
||||
// returning raw output in case other collected information
|
||||
// resulted in errors
|
||||
return disks, output, nil
|
||||
}
|
||||
|
||||
func collectOSInformation(ctx context.Context) (*OsInfo, string, error) {
|
||||
const (
|
||||
architecturePrefix = "OSArchitecture"
|
||||
osSystemPrefix = "Caption"
|
||||
osVersionPrefix = "Version"
|
||||
osReleasePrefix = "BuildNumber"
|
||||
namePrefix = "CSName"
|
||||
)
|
||||
|
||||
command := exec.CommandContext(
|
||||
ctx,
|
||||
"powershell",
|
||||
"-Command",
|
||||
"Get-CimInstance -Class Win32_OperatingSystem | Select-Object OSArchitecture, Caption, Version, BuildNumber, CSName | Format-List",
|
||||
)
|
||||
|
||||
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 := ParseWinOperatingSystemInfo(output, architecturePrefix, osSystemPrefix, osVersionPrefix, osReleasePrefix, namePrefix)
|
||||
if err != nil {
|
||||
return nil, output, err
|
||||
}
|
||||
|
||||
// returning raw output in case other collected information
|
||||
// resulted in errors
|
||||
return osInfo, output, nil
|
||||
}
|
|
@ -15,6 +15,8 @@ import (
|
|||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/net/trace"
|
||||
|
||||
"github.com/cloudflare/cloudflared/diagnostic"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -52,6 +54,7 @@ func GetMetricsKnownAddresses(runtimeType string) [5]string {
|
|||
|
||||
type Config struct {
|
||||
ReadyServer *ReadyServer
|
||||
DiagnosticHandler *diagnostic.Handler
|
||||
QuickTunnelHostname string
|
||||
Orchestrator orchestrator
|
||||
|
||||
|
@ -91,6 +94,8 @@ func newMetricsHandler(
|
|||
})
|
||||
}
|
||||
|
||||
router.HandleFunc("/diag/system", config.DiagnosticHandler.SystemHandler)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue