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:
parent
f85c0f1cc0
commit
a6f9e68739
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue