Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Log HTTP requests/responses at trace level #522

Merged
merged 1 commit into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading