TUN-8735: add managed/local log collection

## Summary
Adds a log collector for the managed/local runtimes.


Closes TUN-8735 TUN-8736
This commit is contained in:
Luis Neto 2024-11-26 10:30:44 -08:00
parent f85c0f1cc0
commit a6f9e68739
7 changed files with 227 additions and 10 deletions

View File

@ -127,6 +127,8 @@ var (
"most likely you already have a conflicting record there. You can also rerun this command with --%s to overwrite "+ "most likely you already have a conflicting record there. You can also rerun this command with --%s to overwrite "+
"any existing DNS records for this hostname.", overwriteDNSFlag) "any existing DNS records for this hostname.", overwriteDNSFlag)
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/)") 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/)")
// TODO: TUN-8756 the list below denotes the flags that do not possess any kind of sensitive information
// however this approach is not maintainble in the long-term.
nonSecretFlagsList = []string{ nonSecretFlagsList = []string{
"config", "config",
"autoupdate-freq", "autoupdate-freq",

101
diagnostic/client.go Normal file
View File

@ -0,0 +1,101 @@
package diagnostic
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"github.com/cloudflare/cloudflared/logger"
)
const configurationEndpoint = "diag/configuration"
type httpClient struct {
http.Client
baseURL url.URL
}
func NewHTTPClient(baseURL url.URL) *httpClient {
httpTransport := http.Transport{
TLSHandshakeTimeout: defaultTimeout,
ResponseHeaderTimeout: defaultTimeout,
}
return &httpClient{
http.Client{
Transport: &httpTransport,
Timeout: defaultTimeout,
},
baseURL,
}
}
func (client *httpClient) GET(ctx context.Context, url string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("error creating GET request: %w", err)
}
req.Header.Add("Accept", "application/json;version=1")
response, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("error GET request: %w", err)
}
return response, nil
}
type LogConfiguration struct {
logFile string
logDirectory string
uid int // the uid of the user that started cloudflared
}
func (client *httpClient) GetLogConfiguration(ctx context.Context) (*LogConfiguration, error) {
endpoint, err := url.JoinPath(client.baseURL.String(), configurationEndpoint)
if err != nil {
return nil, fmt.Errorf("error parsing URL: %w", err)
}
response, err := client.GET(ctx, endpoint)
if err != nil {
return nil, err
}
defer response.Body.Close()
var data map[string]string
if err := json.NewDecoder(response.Body).Decode(&data); err != nil {
return nil, fmt.Errorf("failed to decode body: %w", err)
}
uidStr, exists := data[configurationKeyUID]
if !exists {
return nil, ErrKeyNotFound
}
uid, err := strconv.Atoi(uidStr)
if err != nil {
return nil, fmt.Errorf("error convertin pid to int: %w", err)
}
logFile, exists := data[logger.LogFileFlag]
if exists {
return &LogConfiguration{logFile, "", uid}, nil
}
logDirectory, exists := data[logger.LogDirectoryFlag]
if exists {
return &LogConfiguration{"", logDirectory, uid}, nil
}
return nil, ErrKeyNotFound
}
type HTTPClient interface {
GetLogConfiguration(ctx context.Context) (LogConfiguration, error)
}

View File

@ -8,5 +8,7 @@ const (
systemCollectorName = "system" // used for logging purposes systemCollectorName = "system" // used for logging purposes
tunnelStateCollectorName = "tunnelState" // used for logging purposes tunnelStateCollectorName = "tunnelState" // used for logging purposes
configurationCollectorName = "configuration" // used for logging purposes configurationCollectorName = "configuration" // used for logging purposes
configurationKeyUid = "uid" defaultTimeout = 15 * time.Second // timeout for the collectors
twoWeeksOffset = -14 * 24 * time.Hour // maximum offset for the logs
configurationKeyUID = "uid" // Key used to set and get the UID value from the configuration map
) )

View File

@ -5,12 +5,17 @@ import (
) )
var ( var (
// Error used when there is no log directory available.
ErrManagedLogNotFound = errors.New("managed log directory not found")
// Error used when one key is not found.
ErrMustNotBeEmpty = errors.New("provided argument is empty")
// Error used when parsing the fields of the output of collector. // Error used when parsing the fields of the output of collector.
ErrInsufficientLines = errors.New("insufficient lines") ErrInsufficientLines = errors.New("insufficient lines")
// Error used when parsing the lines of the output of collector. // Error used when parsing the lines of the output of collector.
ErrInsuficientFields = errors.New("insufficient fields") ErrInsuficientFields = errors.New("insufficient fields")
// Error used when given key is not found while parsing KV. // Error used when given key is not found while parsing KV.
ErrKeyNotFound = errors.New("key not found") ErrKeyNotFound = errors.New("key not found")
// Error used when tehre is no disk volume information available // Error used when there is no disk volume information available.
ErrNoVolumeFound = errors.New("No disk volume information found") ErrNoVolumeFound = errors.New("no disk volume information found")
ErrNoPathAvailable = errors.New("no path available")
) )

View File

@ -166,7 +166,7 @@ func (handler *Handler) ConfigurationHandler(writer http.ResponseWriter, _ *http
// The UID is included to help the // The UID is included to help the
// diagnostic tool to understand // diagnostic tool to understand
// if this instance is managed or not. // if this instance is managed or not.
flags[configurationKeyUid] = strconv.Itoa(os.Getuid()) flags[configurationKeyUID] = strconv.Itoa(os.Getuid())
encoder := json.NewEncoder(writer) encoder := json.NewEncoder(writer)
err := encoder.Encode(flags) err := encoder.Encode(flags)

View File

@ -0,0 +1,34 @@
package diagnostic
import (
"context"
)
// Represents the path of the log file or log directory.
// This struct is meant to give some ergonimics regarding
// the logging information.
type LogInformation struct {
path string // path to a file or directory
wasCreated bool // denotes if `path` was created
isDirectory bool // denotes if `path` is a directory
}
func NewLogInformation(
path string,
wasCreated bool,
isDirectory bool,
) *LogInformation {
return &LogInformation{
path,
wasCreated,
isDirectory,
}
}
type LogCollector interface {
// This function is responsible for returning a path to a single file
// whose contents are the logs of a cloudflared instance.
// A new file may be create by a LogCollector, thus, its the caller
// responsibility to remove the newly create file.
Collect(ctx context.Context) (*LogInformation, error)
}

View File

@ -0,0 +1,73 @@
package diagnostic
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
)
const (
linuxManagedLogsPath = "/var/log/cloudflared.err"
darwinManagedLogsPath = "/Library/Logs/com.cloudflare.cloudflared.err.log"
)
type HostLogCollector struct {
client HTTPClient
}
func NewHostLogCollector(client HTTPClient) *HostLogCollector {
return &HostLogCollector{
client,
}
}
func getServiceLogPath() (string, error) {
switch runtime.GOOS {
case "darwin":
{
path := darwinManagedLogsPath
if _, err := os.Stat(path); err == nil {
return path, nil
}
userHomeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("error getting user home: %w", err)
}
return filepath.Join(userHomeDir, darwinManagedLogsPath), nil
}
case "linux":
{
return linuxManagedLogsPath, nil
}
default:
return "", ErrManagedLogNotFound
}
}
func (collector *HostLogCollector) Collect(ctx context.Context) (*LogInformation, error) {
logConfiguration, err := collector.client.GetLogConfiguration(ctx)
if err != nil {
return nil, fmt.Errorf("error getting log configuration: %w", err)
}
if logConfiguration.uid == 0 {
path, err := getServiceLogPath()
if err != nil {
return nil, err
}
return NewLogInformation(path, false, false), nil
}
if logConfiguration.logFile != "" {
return NewLogInformation(logConfiguration.logFile, false, false), nil
} else if logConfiguration.logDirectory != "" {
return NewLogInformation(logConfiguration.logDirectory, false, true), nil
}
return nil, ErrMustNotBeEmpty
}