Skip to content

Commit 02e7ffd

Browse files
committed
TUN-8792: Make diag/system endpoint always return a JSON
## Summary Change the system information collector and respective http handler so that it always returns a JSON. Closes [TUN-8792](https://jira.cfdata.org/browse/TUN-8792)
1 parent ba9f28e commit 02e7ffd

File tree

8 files changed

+288
-141
lines changed

8 files changed

+288
-141
lines changed

diagnostic/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ func (client *httpClient) GetSystemInformation(ctx context.Context, writer io.Wr
141141
return err
142142
}
143143

144-
return copyToWriter(response, writer)
144+
return copyJSONToWriter(response, writer)
145145
}
146146

147147
func (client *httpClient) GetMetrics(ctx context.Context, writer io.Writer) error {

diagnostic/handlers.go

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ func (handler *Handler) InstallEndpoints(router *http.ServeMux) {
5959
router.HandleFunc(systemInformationEndpoint, handler.SystemHandler)
6060
}
6161

62+
type SystemInformationResponse struct {
63+
Info *SystemInformation `json:"info"`
64+
Err error `json:"errors"`
65+
}
66+
6267
func (handler *Handler) SystemHandler(writer http.ResponseWriter, request *http.Request) {
6368
logger := handler.log.With().Str(collectorField, systemCollectorName).Logger()
6469
logger.Info().Msg("Collection started")
@@ -69,30 +74,15 @@ func (handler *Handler) SystemHandler(writer http.ResponseWriter, request *http.
6974

7075
defer cancel()
7176

72-
info, rawInfo, err := handler.systemCollector.Collect(ctx)
73-
if err != nil {
74-
logger.Error().Err(err).Msg("error occurred whilst collecting system information")
75-
76-
if rawInfo != "" {
77-
logger.Info().Msg("using raw information fallback")
78-
bytes := []byte(rawInfo)
79-
writeResponse(writer, bytes, &logger)
80-
} else {
81-
logger.Error().Msg("no raw information available")
82-
writer.WriteHeader(http.StatusInternalServerError)
83-
}
84-
85-
return
86-
}
77+
info, err := handler.systemCollector.Collect(ctx)
8778

88-
if info == nil {
89-
logger.Error().Msgf("system information collection is nil")
90-
writer.WriteHeader(http.StatusInternalServerError)
79+
response := SystemInformationResponse{
80+
Info: info,
81+
Err: err,
9182
}
9283

9384
encoder := json.NewEncoder(writer)
94-
95-
err = encoder.Encode(info)
85+
err = encoder.Encode(response)
9686
if err != nil {
9787
logger.Error().Err(err).Msgf("error occurred whilst serializing information")
9888
writer.WriteHeader(http.StatusInternalServerError)

diagnostic/handlers_test.go

Lines changed: 20 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import (
44
"context"
55
"encoding/json"
66
"errors"
7-
"io"
87
"net"
98
"net/http"
109
"net/http/httptest"
10+
"runtime"
1111
"testing"
1212

1313
"github.com/google/uuid"
@@ -20,11 +20,13 @@ import (
2020
"github.com/cloudflare/cloudflared/tunnelstate"
2121
)
2222

23-
type SystemCollectorMock struct{}
23+
type SystemCollectorMock struct {
24+
systemInfo *diagnostic.SystemInformation
25+
err error
26+
}
2427

2528
const (
2629
systemInformationKey = "sikey"
27-
rawInformationKey = "rikey"
2830
errorKey = "errkey"
2931
)
3032

@@ -46,24 +48,8 @@ func newTrackerFromConns(t *testing.T, connections []tunnelstate.IndexedConnecti
4648
return tracker
4749
}
4850

49-
func setCtxValuesForSystemCollector(
50-
systemInfo *diagnostic.SystemInformation,
51-
rawInfo string,
52-
err error,
53-
) context.Context {
54-
ctx := context.Background()
55-
ctx = context.WithValue(ctx, systemInformationKey, systemInfo)
56-
ctx = context.WithValue(ctx, rawInformationKey, rawInfo)
57-
ctx = context.WithValue(ctx, errorKey, err)
58-
59-
return ctx
60-
}
61-
62-
func (*SystemCollectorMock) Collect(ctx context.Context) (*diagnostic.SystemInformation, string, error) {
63-
si, _ := ctx.Value(systemInformationKey).(*diagnostic.SystemInformation)
64-
ri, _ := ctx.Value(rawInformationKey).(string)
65-
err, _ := ctx.Value(errorKey).(error)
66-
return si, ri, err
51+
func (collector *SystemCollectorMock) Collect(context.Context) (*diagnostic.SystemInformation, error) {
52+
return collector.systemInfo, collector.err
6753
}
6854

6955
func TestSystemHandler(t *testing.T) {
@@ -73,7 +59,6 @@ func TestSystemHandler(t *testing.T) {
7359
tests := []struct {
7460
name string
7561
systemInfo *diagnostic.SystemInformation
76-
rawInfo string
7762
err error
7863
statusCode int
7964
}{
@@ -82,47 +67,39 @@ func TestSystemHandler(t *testing.T) {
8267
systemInfo: diagnostic.NewSystemInformation(
8368
0, 0, 0, 0,
8469
"string", "string", "string", "string",
85-
"string", "string", nil,
70+
"string", "string",
71+
runtime.Version(), runtime.GOARCH, nil,
8672
),
87-
rawInfo: "",
73+
8874
err: nil,
8975
statusCode: http.StatusOK,
9076
},
91-
{
92-
name: "on error and raw info", systemInfo: nil,
93-
rawInfo: "raw info", err: errors.New("an error"), statusCode: http.StatusOK,
94-
},
9577
{
9678
name: "on error and no raw info", systemInfo: nil,
97-
rawInfo: "", err: errors.New("an error"), statusCode: http.StatusInternalServerError,
98-
},
99-
{
100-
name: "malformed response", systemInfo: nil, rawInfo: "", err: nil, statusCode: http.StatusInternalServerError,
79+
err: errors.New("an error"), statusCode: http.StatusOK,
10180
},
10281
}
10382

10483
for _, tCase := range tests {
10584
t.Run(tCase.name, func(t *testing.T) {
10685
t.Parallel()
107-
handler := diagnostic.NewDiagnosticHandler(&log, 0, &SystemCollectorMock{}, uuid.New(), uuid.New(), nil, map[string]string{}, nil)
86+
handler := diagnostic.NewDiagnosticHandler(&log, 0, &SystemCollectorMock{
87+
systemInfo: tCase.systemInfo,
88+
err: tCase.err,
89+
}, uuid.New(), uuid.New(), nil, map[string]string{}, nil)
10890
recorder := httptest.NewRecorder()
109-
ctx := setCtxValuesForSystemCollector(tCase.systemInfo, tCase.rawInfo, tCase.err)
110-
request, err := http.NewRequestWithContext(ctx, http.MethodGet, "/diag/syste,", nil)
91+
ctx := context.Background()
92+
request, err := http.NewRequestWithContext(ctx, http.MethodGet, "/diag/system", nil)
11193
require.NoError(t, err)
11294
handler.SystemHandler(recorder, request)
11395

11496
assert.Equal(t, tCase.statusCode, recorder.Code)
11597
if tCase.statusCode == http.StatusOK && tCase.systemInfo != nil {
116-
var response diagnostic.SystemInformation
117-
98+
var response diagnostic.SystemInformationResponse
11899
decoder := json.NewDecoder(recorder.Body)
119-
err = decoder.Decode(&response)
120-
require.NoError(t, err)
121-
assert.Equal(t, tCase.systemInfo, &response)
122-
} else if tCase.statusCode == http.StatusOK && tCase.rawInfo != "" {
123-
rawBytes, err := io.ReadAll(recorder.Body)
100+
err := decoder.Decode(&response)
124101
require.NoError(t, err)
125-
assert.Equal(t, tCase.rawInfo, string(rawBytes))
102+
assert.Equal(t, tCase.systemInfo, response.Info)
126103
}
127104
})
128105
}

diagnostic/system_collector.go

Lines changed: 97 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,82 @@
11
package diagnostic
22

3-
import "context"
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"strings"
8+
)
9+
10+
type SystemInformationError struct {
11+
Err error `json:"error"`
12+
RawInfo string `json:"rawInfo"`
13+
}
14+
15+
func (err SystemInformationError) Error() string {
16+
return err.Err.Error()
17+
}
18+
19+
func (err SystemInformationError) MarshalJSON() ([]byte, error) {
20+
s := map[string]string{
21+
"error": err.Err.Error(),
22+
"rawInfo": err.RawInfo,
23+
}
24+
25+
return json.Marshal(s)
26+
}
27+
28+
type SystemInformationGeneralError struct {
29+
OperatingSystemInformationError error
30+
MemoryInformationError error
31+
FileDescriptorsInformationError error
32+
DiskVolumeInformationError error
33+
}
34+
35+
func (err SystemInformationGeneralError) Error() string {
36+
builder := &strings.Builder{}
37+
builder.WriteString("errors found:")
38+
39+
if err.OperatingSystemInformationError != nil {
40+
builder.WriteString(err.OperatingSystemInformationError.Error() + ", ")
41+
}
42+
43+
if err.MemoryInformationError != nil {
44+
builder.WriteString(err.MemoryInformationError.Error() + ", ")
45+
}
46+
47+
if err.FileDescriptorsInformationError != nil {
48+
builder.WriteString(err.FileDescriptorsInformationError.Error() + ", ")
49+
}
50+
51+
if err.DiskVolumeInformationError != nil {
52+
builder.WriteString(err.DiskVolumeInformationError.Error() + ", ")
53+
}
54+
55+
return builder.String()
56+
}
57+
58+
func (err SystemInformationGeneralError) MarshalJSON() ([]byte, error) {
59+
data := map[string]SystemInformationError{}
60+
61+
var sysErr SystemInformationError
62+
if errors.As(err.OperatingSystemInformationError, &sysErr) {
63+
data["operatingSystemInformationError"] = sysErr
64+
}
65+
66+
if errors.As(err.MemoryInformationError, &sysErr) {
67+
data["memoryInformationError"] = sysErr
68+
}
69+
70+
if errors.As(err.FileDescriptorsInformationError, &sysErr) {
71+
data["fileDescriptorsInformationError"] = sysErr
72+
}
73+
74+
if errors.As(err.DiskVolumeInformationError, &sysErr) {
75+
data["diskVolumeInformationError"] = sysErr
76+
}
77+
78+
return json.Marshal(data)
79+
}
480

581
type DiskVolumeInformation struct {
682
Name string `json:"name"` // represents the filesystem in linux/macos or device name in windows
@@ -17,17 +93,19 @@ func NewDiskVolumeInformation(name string, maximum, current uint64) *DiskVolumeI
1793
}
1894

1995
type SystemInformation struct {
20-
MemoryMaximum uint64 `json:"memoryMaximum"` // represents the maximum memory of the system in kilobytes
21-
MemoryCurrent uint64 `json:"memoryCurrent"` // represents the system's memory in use in kilobytes
22-
FileDescriptorMaximum uint64 `json:"fileDescriptorMaximum"` // represents the maximum number of file descriptors of the system
23-
FileDescriptorCurrent uint64 `json:"fileDescriptorCurrent"` // represents the system's file descriptors in use
24-
OsSystem string `json:"osSystem"` // represents the operating system name i.e.: linux, windows, darwin
25-
HostName string `json:"hostName"` // represents the system host name
26-
OsVersion string `json:"osVersion"` // detailed information about the system's release version level
27-
OsRelease string `json:"osRelease"` // detailed information about the system's release
28-
Architecture string `json:"architecture"` // represents the system's hardware platform i.e: arm64/amd64
29-
CloudflaredVersion string `json:"cloudflaredVersion"` // the runtime version of cloudflared
30-
Disk []*DiskVolumeInformation `json:"disk"`
96+
MemoryMaximum uint64 `json:"memoryMaximum,omitempty"` // represents the maximum memory of the system in kilobytes
97+
MemoryCurrent uint64 `json:"memoryCurrent,omitempty"` // represents the system's memory in use in kilobytes
98+
FileDescriptorMaximum uint64 `json:"fileDescriptorMaximum,omitempty"` // represents the maximum number of file descriptors of the system
99+
FileDescriptorCurrent uint64 `json:"fileDescriptorCurrent,omitempty"` // represents the system's file descriptors in use
100+
OsSystem string `json:"osSystem,omitempty"` // represents the operating system name i.e.: linux, windows, darwin
101+
HostName string `json:"hostName,omitempty"` // represents the system host name
102+
OsVersion string `json:"osVersion,omitempty"` // detailed information about the system's release version level
103+
OsRelease string `json:"osRelease,omitempty"` // detailed information about the system's release
104+
Architecture string `json:"architecture,omitempty"` // represents the system's hardware platform i.e: arm64/amd64
105+
CloudflaredVersion string `json:"cloudflaredVersion,omitempty"` // the runtime version of cloudflared
106+
GoVersion string `json:"goVersion,omitempty"`
107+
GoArch string `json:"goArch,omitempty"`
108+
Disk []*DiskVolumeInformation `json:"disk,omitempty"`
31109
}
32110

33111
func NewSystemInformation(
@@ -40,7 +118,9 @@ func NewSystemInformation(
40118
osVersion,
41119
osRelease,
42120
architecture,
43-
cloudflaredVersion string,
121+
cloudflaredVersion,
122+
goVersion,
123+
goArchitecture string,
44124
disk []*DiskVolumeInformation,
45125
) *SystemInformation {
46126
return &SystemInformation{
@@ -54,17 +134,17 @@ func NewSystemInformation(
54134
osRelease,
55135
architecture,
56136
cloudflaredVersion,
137+
goVersion,
138+
goArchitecture,
57139
disk,
58140
}
59141
}
60142

61143
type SystemCollector interface {
62144
// If the collection is successful it will return `SystemInformation` struct,
63-
// an empty string, and a nil error.
64-
// In case there is an error a string with the raw data will be returned
65-
// however the returned string not contain all the data points.
145+
// and a nil error.
66146
//
67147
// This function expects that the caller sets the context timeout to prevent
68148
// long-lived collectors.
69-
Collect(ctx context.Context) (*SystemInformation, string, error)
149+
Collect(ctx context.Context) (*SystemInformation, error)
70150
}

0 commit comments

Comments
 (0)