diff --git a/tfe/logging.go b/tfe/logging.go new file mode 100644 index 000000000..b77e6818b --- /dev/null +++ b/tfe/logging.go @@ -0,0 +1,98 @@ +package tfe + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "net/http/httputil" + "os" + "strings" +) + +type loggingTransport struct { + name string + delegate http.RoundTripper +} + +const ( + EnvLog = "TF_LOG" +) + +// redactedHeaders is a list of lowercase headers (with trailing colons) that signal that the +// header values should be redacted from logs +var redactedHeaders = []string{"authorization:", "proxy-authorization:"} + +// IsDebugOrHigher returns whether or not the current log level is debug or trace +func IsDebugOrHigher() bool { + level := strings.ToUpper(os.Getenv(EnvLog)) + return level == "DEBUG" || level == "TRACE" +} + +// RoundTrip is a transport method that logs the request and response if the TF_LOG level is +// TRACE or DEBUG +func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if IsDebugOrHigher() { + reqData, err := httputil.DumpRequestOut(req, true) + if err == nil { + log.Printf("[DEBUG] "+logReqMsg, t.name, filterAndPrettyPrintLines(reqData)) + } else { + log.Printf("[ERROR] %s API Request error: %#v", t.name, err) + } + } + + resp, err := t.delegate.RoundTrip(req) + if err != nil { + return resp, err + } + + if IsDebugOrHigher() { + respData, err := httputil.DumpResponse(resp, true) + if err == nil { + log.Printf("[DEBUG] "+logRespMsg, t.name, filterAndPrettyPrintLines(respData)) + } else { + log.Printf("[ERROR] %s API Response error: %#v", t.name, err) + } + } + + return resp, nil +} + +// NewLoggingTransport wraps the given transport with a logger that logs request and +// response details +func NewLoggingTransport(name string, t http.RoundTripper) *loggingTransport { + return &loggingTransport{name, t} +} + +// filterAndPrettyPrintLines iterates through a []byte line-by-line, +// redacting any sensitive lines and transforming any lines that are complete json into +// pretty-printed json. +func filterAndPrettyPrintLines(b []byte) string { + parts := strings.Split(string(b), "\n") + for i, p := range parts { + for _, check := range redactedHeaders { + if strings.HasPrefix(strings.ToLower(p), check) { + // This looks like a sensitive header to redact, so overwrite the entire line + parts[i] = fmt.Sprintf("%s ", p[0:len(check)]) + continue + } + } + if b := []byte(p); json.Valid(b) { + var out bytes.Buffer + _ = json.Indent(&out, b, "", " ") // already checked for validity + parts[i] = out.String() + } + } + return strings.Join(parts, "\n") +} + +const logReqMsg = `%s API Request Details: +---[ REQUEST ]--------------------------------------- +%s +-----------------------------------------------------` + +const logRespMsg = `%s API Response Details: +---[ RESPONSE ]-------------------------------------- +%s +-----------------------------------------------------` diff --git a/tfe/provider.go b/tfe/provider.go index 5290960b8..d7a9aed5e 100644 --- a/tfe/provider.go +++ b/tfe/provider.go @@ -17,7 +17,6 @@ import ( tfe "github.com/hashicorp/go-tfe" version "github.com/hashicorp/go-version" "github.com/hashicorp/hcl" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" providerVersion "github.com/hashicorp/terraform-provider-tfe/version" svchost "github.com/hashicorp/terraform-svchost" @@ -184,7 +183,7 @@ func getClient(tfeHost, token string, insecure bool) (*tfe.Client, error) { credsSrc := credentialsSource(config) services := disco.NewWithCredentialsSource(credsSrc) services.SetUserAgent(providerUaString) - services.Transport = logging.NewTransport("TFE Discovery", transport) + services.Transport = NewLoggingTransport("TFE Discovery", transport) // Add any static host configurations service discovery object. for userHost, hostConfig := range config.Hosts { @@ -265,7 +264,7 @@ func getClient(tfeHost, token string, insecure bool) (*tfe.Client, error) { } // Wrap the configured transport to enable logging. - httpClient.Transport = logging.NewTransport("TFE", transport) + httpClient.Transport = NewLoggingTransport("TFE", transport) // Create a new TFE client config cfg := &tfe.Config{