Skip to content

Commit

Permalink
Log HTTP requests/responses at trace level
Browse files Browse the repository at this point in the history
  • Loading branch information
K-Phoen committed Nov 13, 2024
1 parent 26e89e4 commit 7c045bd
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 20 deletions.
3 changes: 3 additions & 0 deletions cmd/grr/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"

"github.com/go-clix/cli"
"github.com/grafana/grizzly/internal/logger"
"github.com/grafana/grizzly/pkg/config"
"github.com/grafana/grizzly/pkg/grafana"
"github.com/grafana/grizzly/pkg/grizzly"
Expand Down Expand Up @@ -49,6 +50,8 @@ func main() {
log.Fatalln(err)
}

log.AddHook(logger.NewSecretsRedactor(context.Secrets()))

registry := createRegistry(context)
// workflow commands
rootCmd.AddCommand(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package syntheticmonitoring
package httputils

import (
"net/http"
Expand All @@ -7,14 +7,23 @@ import (
"time"
)

var defaultTimeout = 10 * time.Second

func NewHTTPClient() (*http.Client, error) {
timeout := 10 * time.Second
timeout := defaultTimeout

// TODO: Move this configuration to the global configuration
if timeoutStr := os.Getenv("GRIZZLY_HTTP_TIMEOUT"); timeoutStr != "" {
timeoutSeconds, err := strconv.Atoi(timeoutStr)
if err != nil {
return nil, err
}

timeout = time.Duration(timeoutSeconds) * time.Second
}
return &http.Client{Timeout: timeout}, nil

return &http.Client{
Timeout: timeout,
Transport: &LoggedHTTPRoundTripper{},
}, nil
}
32 changes: 32 additions & 0 deletions internal/httputils/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package httputils

import (
"net/http"
"net/http/httputil"

log "github.com/sirupsen/logrus"
)

type LoggedHTTPRoundTripper struct {
DecoratedTransport http.RoundTripper
}

func (rt LoggedHTTPRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
transport := http.DefaultTransport
if rt.DecoratedTransport != nil {
transport = rt.DecoratedTransport
}

reqStr, _ := httputil.DumpRequest(req, true)
log.Traceln(string(reqStr))

resp, err := transport.RoundTrip(req)
if err != nil {
return resp, err
}

respStr, _ := httputil.DumpResponse(resp, true)
log.Traceln(string(respStr))

return resp, err
}
101 changes: 101 additions & 0 deletions internal/logger/redact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package logger

import (
"reflect"
"strings"

"github.com/sirupsen/logrus"
)

type SecretsRedactor struct {
replacementsMap map[string]string
}

func NewSecretsRedactor(secrets []string) *SecretsRedactor {
redactor := &SecretsRedactor{
replacementsMap: make(map[string]string, len(secrets)),
}

for _, secret := range secrets {
replacement := "**REDACTED**"
if len(secret) >= 20 {
replacement = secret[:9] + "..." + secret[len(secret)-5:]
}
redactor.replacementsMap[secret] = replacement
}

return redactor
}

func (h *SecretsRedactor) Levels() []logrus.Level {
return logrus.AllLevels
}

func (h *SecretsRedactor) Fire(entry *logrus.Entry) error {
entry.Message = h.redactString(entry.Message)

for key, value := range entry.Data {
entry.Data[key] = h.redactValue(reflect.ValueOf(value))
}

return nil
}

func (h *SecretsRedactor) redactValue(v reflect.Value) any {
if h.reflectValueIsNil(v) {
return nil
}

if !v.IsValid() {
return nil
}

// TODO: this is far from exhaustive :(
switch v.Kind() {
case reflect.String:
return h.redactString(v.String())

case reflect.Struct:
newStruct := reflect.New(v.Type()).Elem()
h.redactStructFields(v, newStruct)
return newStruct.Interface()

case reflect.Slice:
newSlice := reflect.MakeSlice(v.Type(), v.Len(), v.Len())
for i := 0; i < v.Len(); i++ {
newSlice.Index(i).Set(reflect.ValueOf(h.redactValue(v.Index(i))))
}

return newSlice.Interface()

case reflect.Map:
newMap := reflect.MakeMap(v.Type())
iter := v.MapRange()
for iter.Next() {
newV := h.redactValue(iter.Value())
newMap.SetMapIndex(iter.Key(), reflect.ValueOf(newV))
}

return newMap.Interface()
}

return v.Interface()
}

func (h *SecretsRedactor) redactStructFields(src, dest reflect.Value) {
for i := 0; i < src.NumField(); i++ {
dest.Field(i).Set(reflect.ValueOf(h.redactValue(src.Field(i))))
}
}

func (h *SecretsRedactor) redactString(s string) string {
for secret, redacted := range h.replacementsMap {
s = strings.ReplaceAll(s, secret, redacted)
}
return s
}

func (h *SecretsRedactor) reflectValueIsNil(value reflect.Value) bool {
kind := value.Kind()
return (kind == reflect.Pointer || kind == reflect.Interface || kind == reflect.Array || kind == reflect.Slice || kind == reflect.Map) && value.IsNil()
}
20 changes: 20 additions & 0 deletions pkg/config/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,23 @@ type Context struct {
ResourceKind string `yaml:"resource-kind" mapstructure:"resource-kind"`
FolderUID string `yaml:"folder-uid" mapstructure:"folder-uid"`
}

// Secrets returns all the secrets contained in the current context.
// This is mainly useful to be able to redact those from logs.
func (c Context) Secrets() []string {
candidates := []string{
c.Grafana.Token,
c.Mimir.APIKey,
c.SyntheticMonitoring.Token,
c.SyntheticMonitoring.AccessToken,
}

secrets := make([]string, 0, len(candidates))
for _, candidate := range candidates {
if candidate != "" {
secrets = append(secrets, candidate)
}
}

return secrets
}
7 changes: 7 additions & 0 deletions pkg/grafana/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

gclient "github.com/grafana/grafana-openapi-client-go/client"
"github.com/grafana/grafana-openapi-client-go/client/dashboards"
"github.com/grafana/grizzly/internal/httputils"
"github.com/grafana/grizzly/pkg/config"
"github.com/grafana/grizzly/pkg/grizzly"
)
Expand Down Expand Up @@ -95,6 +96,12 @@ func (p *Provider) Client() (*gclient.GrafanaHTTPAPI, error) {
WithSchemes([]string{parsedURL.Scheme}).
WithBasePath(filepath.Join(parsedURL.Path, "api"))

httpClient, err := httputils.NewHTTPClient()
if err != nil {
return nil, err
}
transportConfig.Client = httpClient

if parsedURL.Scheme == "https" && p.config.InsecureSkipVerify {
transportConfig.TLSConfig = &tls.Config{
InsecureSkipVerify: true,
Expand Down
25 changes: 9 additions & 16 deletions pkg/mimir/client/http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import (
"io"
"net/http"
"os"
"strconv"
"strings"
"time"

"github.com/grafana/grizzly/internal/httputils"
"github.com/grafana/grizzly/pkg/config"
"github.com/grafana/grizzly/pkg/mimir/models"
"github.com/hashicorp/go-multierror"
Expand Down Expand Up @@ -124,22 +123,14 @@ func (c *Client) doRequest(method string, url string, body []byte) ([]byte, erro
}

func (c *Client) createHTTPClient() (*http.Client, error) {
timeout := 10 * time.Second
// TODO: Move this configuration to the global configuration
if timeoutStr := os.Getenv("GRIZZLY_HTTP_TIMEOUT"); timeoutStr != "" {
timeoutSeconds, err := strconv.Atoi(timeoutStr)
if err != nil {
return nil, err
}
timeout = time.Duration(timeoutSeconds) * time.Second
}

tlsConfig := &tls.Config{}
httpClient := http.Client{
Timeout: timeout,
Transport: &http.Transport{TLSClientConfig: tlsConfig},
httpClient, err := httputils.NewHTTPClient()
if err != nil {
return nil, err
}

httpClient.Transport = &http.Transport{TLSClientConfig: tlsConfig}

if c.config.TLS.CAPath != "" {
certPool, err := x509.SystemCertPool()
if err != nil {
Expand All @@ -164,11 +155,13 @@ func (c *Client) createHTTPClient() (*http.Client, error) {
if err != nil {
return nil, err
}

tlsConfig.Certificates = []tls.Certificate{clientTLSCert}
}

httpClient.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
return &httpClient, nil

return httpClient, nil
}
3 changes: 2 additions & 1 deletion pkg/syntheticmonitoring/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"path/filepath"
"time"

"github.com/grafana/grizzly/internal/httputils"
"github.com/grafana/grizzly/pkg/config"
"github.com/grafana/grizzly/pkg/grizzly"
smapi "github.com/grafana/synthetic-monitoring-api-go-client"
Expand Down Expand Up @@ -101,7 +102,7 @@ func (p *Provider) GetHandlers() []grizzly.Handler {

// NewClient creates a new client for synthetic monitoring go client
func (p *Provider) Client() (*smapi.Client, error) {
client, err := NewHTTPClient()
client, err := httputils.NewHTTPClient()
if err != nil {
return nil, err
}
Expand Down

0 comments on commit 7c045bd

Please sign in to comment.