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/config"
 | 
				
			||||||
	"github.com/cloudflare/cloudflared/connection"
 | 
						"github.com/cloudflare/cloudflared/connection"
 | 
				
			||||||
	"github.com/cloudflare/cloudflared/credentials"
 | 
						"github.com/cloudflare/cloudflared/credentials"
 | 
				
			||||||
 | 
						"github.com/cloudflare/cloudflared/diagnostic"
 | 
				
			||||||
	"github.com/cloudflare/cloudflared/edgediscovery"
 | 
						"github.com/cloudflare/cloudflared/edgediscovery"
 | 
				
			||||||
	"github.com/cloudflare/cloudflared/features"
 | 
						"github.com/cloudflare/cloudflared/features"
 | 
				
			||||||
	"github.com/cloudflare/cloudflared/ingress"
 | 
						"github.com/cloudflare/cloudflared/ingress"
 | 
				
			||||||
| 
						 | 
					@ -463,8 +464,10 @@ func StartServer(
 | 
				
			||||||
		readinessServer := metrics.NewReadyServer(clientID,
 | 
							readinessServer := metrics.NewReadyServer(clientID,
 | 
				
			||||||
			tunnelstate.NewConnTracker(log))
 | 
								tunnelstate.NewConnTracker(log))
 | 
				
			||||||
		observer.RegisterSink(readinessServer)
 | 
							observer.RegisterSink(readinessServer)
 | 
				
			||||||
 | 
							diagnosticHandler := diagnostic.NewDiagnosticHandler(log, 0, diagnostic.NewSystemCollectorImpl(buildInfo.CloudflaredVersion))
 | 
				
			||||||
		metricsConfig := metrics.Config{
 | 
							metricsConfig := metrics.Config{
 | 
				
			||||||
			ReadyServer:         readinessServer,
 | 
								ReadyServer:         readinessServer,
 | 
				
			||||||
 | 
								DiagnosticHandler:   diagnosticHandler,
 | 
				
			||||||
			QuickTunnelHostname: quickTunnelURL,
 | 
								QuickTunnelHostname: quickTunnelURL,
 | 
				
			||||||
			Orchestrator:        orchestrator,
 | 
								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/prometheus/client_golang/prometheus/promhttp"
 | 
				
			||||||
	"github.com/rs/zerolog"
 | 
						"github.com/rs/zerolog"
 | 
				
			||||||
	"golang.org/x/net/trace"
 | 
						"golang.org/x/net/trace"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/cloudflare/cloudflared/diagnostic"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
| 
						 | 
					@ -52,6 +54,7 @@ func GetMetricsKnownAddresses(runtimeType string) [5]string {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Config struct {
 | 
					type Config struct {
 | 
				
			||||||
	ReadyServer         *ReadyServer
 | 
						ReadyServer         *ReadyServer
 | 
				
			||||||
 | 
						DiagnosticHandler   *diagnostic.Handler
 | 
				
			||||||
	QuickTunnelHostname string
 | 
						QuickTunnelHostname string
 | 
				
			||||||
	Orchestrator        orchestrator
 | 
						Orchestrator        orchestrator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -91,6 +94,8 @@ func newMetricsHandler(
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						router.HandleFunc("/diag/system", config.DiagnosticHandler.SystemHandler)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return router
 | 
						return router
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue