Skip to content

Commit a6f9e68

Browse files
committed
TUN-8735: add managed/local log collection
## Summary Adds a log collector for the managed/local runtimes. Closes TUN-8735 TUN-8736
1 parent f85c0f1 commit a6f9e68

File tree

7 files changed

+227
-10
lines changed

7 files changed

+227
-10
lines changed

cmd/cloudflared/tunnel/cmd.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ var (
127127
"most likely you already have a conflicting record there. You can also rerun this command with --%s to overwrite "+
128128
"any existing DNS records for this hostname.", overwriteDNSFlag)
129129
deprecatedClassicTunnelErr = fmt.Errorf("Classic tunnels have been deprecated, please use Named Tunnels. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/)")
130-
nonSecretFlagsList = []string{
130+
// TODO: TUN-8756 the list below denotes the flags that do not possess any kind of sensitive information
131+
// however this approach is not maintainble in the long-term.
132+
nonSecretFlagsList = []string{
131133
"config",
132134
"autoupdate-freq",
133135
"no-autoupdate",

diagnostic/client.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package diagnostic
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"net/url"
9+
"strconv"
10+
11+
"github.com/cloudflare/cloudflared/logger"
12+
)
13+
14+
const configurationEndpoint = "diag/configuration"
15+
16+
type httpClient struct {
17+
http.Client
18+
baseURL url.URL
19+
}
20+
21+
func NewHTTPClient(baseURL url.URL) *httpClient {
22+
httpTransport := http.Transport{
23+
TLSHandshakeTimeout: defaultTimeout,
24+
ResponseHeaderTimeout: defaultTimeout,
25+
}
26+
27+
return &httpClient{
28+
http.Client{
29+
Transport: &httpTransport,
30+
Timeout: defaultTimeout,
31+
},
32+
baseURL,
33+
}
34+
}
35+
36+
func (client *httpClient) GET(ctx context.Context, url string) (*http.Response, error) {
37+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
38+
if err != nil {
39+
return nil, fmt.Errorf("error creating GET request: %w", err)
40+
}
41+
42+
req.Header.Add("Accept", "application/json;version=1")
43+
44+
response, err := client.Do(req)
45+
if err != nil {
46+
return nil, fmt.Errorf("error GET request: %w", err)
47+
}
48+
49+
return response, nil
50+
}
51+
52+
type LogConfiguration struct {
53+
logFile string
54+
logDirectory string
55+
uid int // the uid of the user that started cloudflared
56+
}
57+
58+
func (client *httpClient) GetLogConfiguration(ctx context.Context) (*LogConfiguration, error) {
59+
endpoint, err := url.JoinPath(client.baseURL.String(), configurationEndpoint)
60+
if err != nil {
61+
return nil, fmt.Errorf("error parsing URL: %w", err)
62+
}
63+
64+
response, err := client.GET(ctx, endpoint)
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
defer response.Body.Close()
70+
71+
var data map[string]string
72+
if err := json.NewDecoder(response.Body).Decode(&data); err != nil {
73+
return nil, fmt.Errorf("failed to decode body: %w", err)
74+
}
75+
76+
uidStr, exists := data[configurationKeyUID]
77+
if !exists {
78+
return nil, ErrKeyNotFound
79+
}
80+
81+
uid, err := strconv.Atoi(uidStr)
82+
if err != nil {
83+
return nil, fmt.Errorf("error convertin pid to int: %w", err)
84+
}
85+
86+
logFile, exists := data[logger.LogFileFlag]
87+
if exists {
88+
return &LogConfiguration{logFile, "", uid}, nil
89+
}
90+
91+
logDirectory, exists := data[logger.LogDirectoryFlag]
92+
if exists {
93+
return &LogConfiguration{"", logDirectory, uid}, nil
94+
}
95+
96+
return nil, ErrKeyNotFound
97+
}
98+
99+
type HTTPClient interface {
100+
GetLogConfiguration(ctx context.Context) (LogConfiguration, error)
101+
}

diagnostic/consts.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package diagnostic
33
import "time"
44

55
const (
6-
defaultCollectorTimeout = time.Second * 10 // This const define the timeout value of a collector operation.
7-
collectorField = "collector" // used for logging purposes
8-
systemCollectorName = "system" // used for logging purposes
9-
tunnelStateCollectorName = "tunnelState" // used for logging purposes
10-
configurationCollectorName = "configuration" // used for logging purposes
11-
configurationKeyUid = "uid"
6+
defaultCollectorTimeout = time.Second * 10 // This const define the timeout value of a collector operation.
7+
collectorField = "collector" // used for logging purposes
8+
systemCollectorName = "system" // used for logging purposes
9+
tunnelStateCollectorName = "tunnelState" // used for logging purposes
10+
configurationCollectorName = "configuration" // used for logging purposes
11+
defaultTimeout = 15 * time.Second // timeout for the collectors
12+
twoWeeksOffset = -14 * 24 * time.Hour // maximum offset for the logs
13+
configurationKeyUID = "uid" // Key used to set and get the UID value from the configuration map
1214
)

diagnostic/error.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@ import (
55
)
66

77
var (
8+
// Error used when there is no log directory available.
9+
ErrManagedLogNotFound = errors.New("managed log directory not found")
10+
// Error used when one key is not found.
11+
ErrMustNotBeEmpty = errors.New("provided argument is empty")
812
// Error used when parsing the fields of the output of collector.
913
ErrInsufficientLines = errors.New("insufficient lines")
1014
// Error used when parsing the lines of the output of collector.
1115
ErrInsuficientFields = errors.New("insufficient fields")
1216
// Error used when given key is not found while parsing KV.
1317
ErrKeyNotFound = errors.New("key not found")
14-
// Error used when tehre is no disk volume information available
15-
ErrNoVolumeFound = errors.New("No disk volume information found")
18+
// Error used when there is no disk volume information available.
19+
ErrNoVolumeFound = errors.New("no disk volume information found")
20+
ErrNoPathAvailable = errors.New("no path available")
1621
)

diagnostic/handlers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ func (handler *Handler) ConfigurationHandler(writer http.ResponseWriter, _ *http
166166
// The UID is included to help the
167167
// diagnostic tool to understand
168168
// if this instance is managed or not.
169-
flags[configurationKeyUid] = strconv.Itoa(os.Getuid())
169+
flags[configurationKeyUID] = strconv.Itoa(os.Getuid())
170170
encoder := json.NewEncoder(writer)
171171

172172
err := encoder.Encode(flags)

diagnostic/log_collector.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package diagnostic
2+
3+
import (
4+
"context"
5+
)
6+
7+
// Represents the path of the log file or log directory.
8+
// This struct is meant to give some ergonimics regarding
9+
// the logging information.
10+
type LogInformation struct {
11+
path string // path to a file or directory
12+
wasCreated bool // denotes if `path` was created
13+
isDirectory bool // denotes if `path` is a directory
14+
}
15+
16+
func NewLogInformation(
17+
path string,
18+
wasCreated bool,
19+
isDirectory bool,
20+
) *LogInformation {
21+
return &LogInformation{
22+
path,
23+
wasCreated,
24+
isDirectory,
25+
}
26+
}
27+
28+
type LogCollector interface {
29+
// This function is responsible for returning a path to a single file
30+
// whose contents are the logs of a cloudflared instance.
31+
// A new file may be create by a LogCollector, thus, its the caller
32+
// responsibility to remove the newly create file.
33+
Collect(ctx context.Context) (*LogInformation, error)
34+
}

diagnostic/log_collector_host.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package diagnostic
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"runtime"
9+
)
10+
11+
const (
12+
linuxManagedLogsPath = "/var/log/cloudflared.err"
13+
darwinManagedLogsPath = "/Library/Logs/com.cloudflare.cloudflared.err.log"
14+
)
15+
16+
type HostLogCollector struct {
17+
client HTTPClient
18+
}
19+
20+
func NewHostLogCollector(client HTTPClient) *HostLogCollector {
21+
return &HostLogCollector{
22+
client,
23+
}
24+
}
25+
26+
func getServiceLogPath() (string, error) {
27+
switch runtime.GOOS {
28+
case "darwin":
29+
{
30+
path := darwinManagedLogsPath
31+
if _, err := os.Stat(path); err == nil {
32+
return path, nil
33+
}
34+
35+
userHomeDir, err := os.UserHomeDir()
36+
if err != nil {
37+
return "", fmt.Errorf("error getting user home: %w", err)
38+
}
39+
40+
return filepath.Join(userHomeDir, darwinManagedLogsPath), nil
41+
}
42+
case "linux":
43+
{
44+
return linuxManagedLogsPath, nil
45+
}
46+
default:
47+
return "", ErrManagedLogNotFound
48+
}
49+
}
50+
51+
func (collector *HostLogCollector) Collect(ctx context.Context) (*LogInformation, error) {
52+
logConfiguration, err := collector.client.GetLogConfiguration(ctx)
53+
if err != nil {
54+
return nil, fmt.Errorf("error getting log configuration: %w", err)
55+
}
56+
57+
if logConfiguration.uid == 0 {
58+
path, err := getServiceLogPath()
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
return NewLogInformation(path, false, false), nil
64+
}
65+
66+
if logConfiguration.logFile != "" {
67+
return NewLogInformation(logConfiguration.logFile, false, false), nil
68+
} else if logConfiguration.logDirectory != "" {
69+
return NewLogInformation(logConfiguration.logDirectory, false, true), nil
70+
}
71+
72+
return nil, ErrMustNotBeEmpty
73+
}

0 commit comments

Comments
 (0)