Skip to content

Commit

Permalink
Support Atlas client customization & logging
Browse files Browse the repository at this point in the history
Signed-off-by: Jose Vazquez <jose.vazquez@mongodb.com>
  • Loading branch information
josvazg committed Jul 10, 2023
1 parent a10bf50 commit dfe4b1e
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 1 deletion.
15 changes: 14 additions & 1 deletion pkg/controller/atlas/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ import (
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/httputil"
)

type HTTPClientFn func() *http.Client

// CustomHTTPClientFn allows to override the default basic http.Client,
// for protocol logging or test mocking
var CustomHTTPClientFn HTTPClientFn

// Client is the central place to create a client for Atlas using specified API keys and a server URL.
// Note, that the default HTTP transport is reused globally by Go so all caching, keep-alive etc will be in action.
func Client(atlasDomain string, connection Connection, log *zap.SugaredLogger) (mongodbatlas.Client, error) {
withDigest := httputil.Digest(connection.PublicKey, connection.PrivateKey)
withLogging := httputil.LoggingTransport(log)

httpClient, err := httputil.DecorateClient(basicClient(), withDigest, withLogging)
httpClient, err := httputil.DecorateClient(newClient(), withDigest, withLogging)
if err != nil {
return mongodbatlas.Client{}, err
}
Expand All @@ -36,3 +42,10 @@ func basicClient() *http.Client {
// Do we need any custom configuration of timeout etc?
return &http.Client{Transport: http.DefaultTransport}
}

func newClient() *http.Client {
if CustomHTTPClientFn != nil {
return CustomHTTPClientFn()
}
return basicClient()
}
13 changes: 13 additions & 0 deletions pkg/controller/atlas/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/mongodb/mongodb-atlas-kubernetes/pkg/version"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/atlas"
Expand All @@ -17,3 +18,15 @@ func TestClientUserAgent(t *testing.T) {
r.NoError(err)
r.Contains(c.UserAgent, version.Version)
}

func TestCustomClient(t *testing.T) {
cl := atlas.NewDefaultClientLogger()
atlas.CustomHTTPClientFn = atlas.LoggedClientBuilder(cl)
defer func() {
atlas.CustomHTTPClientFn = nil
}()

_, err := atlas.Client("https://cloud.mongodb.com", atlas.Connection{}, nil)
assert.NoError(t, err)
// TODO: actually check the underlying client was customized
}
101 changes: 101 additions & 0 deletions pkg/controller/atlas/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package atlas

import (
"fmt"
"log"
"net/http"
"net/http/httputil"
"os"
"strings"
)

func dumpRequest(req *http.Request) string {
if req == nil {
return "<nil>"
}
requestDump, err := httputil.DumpRequest(req, true)
if err != nil {
return err.Error()
}
return string(requestDump)
}

func dumpResponse(rsp *http.Response) string {
if rsp == nil {
return "<nil>"
}
responseDump, err := httputil.DumpResponse(rsp, true)
if err != nil {
return err.Error()
}
return string(responseDump)
}

// AtlasLog is all is needed to implement logging
type AtlasLog interface {
Printf(format string, v ...any)
}

// ClientLogger is a wrapper or a http.RoundTripper that logs all interactions
type ClientLogger struct {
wrapped http.RoundTripper
logger AtlasLog
}

// NewDefaultClientLogger creates a new ClientLogger with default http transport and logger
func NewDefaultClientLogger() *ClientLogger {
return NewClientLogger(http.DefaultTransport, NewDefaultFileLog())
}

// NewClientLogger creates a new ClientLogger instance
func NewClientLogger(roundTripper http.RoundTripper, logger AtlasLog) *ClientLogger {
return &ClientLogger{wrapped: roundTripper, logger: logger}
}

// RoundTrip implements http.RoundTripper for ClientLogger
func (cl *ClientLogger) RoundTrip(req *http.Request) (*http.Response, error) {
cl.logger.Printf("SENT To Atlas Request:\n%s\n", dumpRequest(req))
rsp, err := cl.wrapped.RoundTrip(req)
cl.logger.Printf("RECV From Atlas Response:\n%s\n", dumpResponse(rsp))
return rsp, err
}

// FileLog dumps interactions to both standard log and a named file
type FileLog struct {
filename string
}

// NewDefaultFileLog returns a FileLog to "./atlas-http.log"
func NewDefaultFileLog() *FileLog {
return NewFileLog("./atlas-http.log")
}

// NewFileLog returns a FileLog to the given filename
func NewFileLog(filename string) *FileLog {
return &FileLog{filename: filename}
}

// Printf implements AtlasLog for FileLog
func (fl *FileLog) Printf(format string, args ...any) {
log.Printf(format, args...)
f, err := os.OpenFile(fl.filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
log.Printf("failed log to filename: %v\n", err)
return
}
defer f.Close()
fmt.Fprintf(f, format, args...)
if !strings.HasSuffix(format, "\n") {
fmt.Fprintln(f)
}
}

// LoggedClientBuilder returns a function to create logged clients.
// Usage example:
//
// atlas.CustomHTTPClientFn = atlas.LoggedClientBuilder(atlas.NewDefaultClientLogger())
func LoggedClientBuilder(cl *ClientLogger) HTTPClientFn {
return func() *http.Client {
return &http.Client{Transport: cl}
}
}
85 changes: 85 additions & 0 deletions pkg/controller/atlas/logger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package atlas_test

import (
"bytes"
"fmt"
"net/http"
"os"
"testing"

"github.com/stretchr/testify/assert"

"github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/atlas"
)

func TestNewDefaultLogger(t *testing.T) {
assert.Equal(t,
atlas.NewClientLogger(http.DefaultTransport, atlas.NewDefaultFileLog()),
atlas.NewDefaultClientLogger(),
)
}

type fakeRoundTripper struct{}

func (frt *fakeRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return nil, nil
}

type fakeLogger struct {
buf *bytes.Buffer
}

func NewFakeLogger() *fakeLogger {
return &fakeLogger{buf: bytes.NewBufferString("")}
}

func (fl *fakeLogger) Printf(format string, v ...any) {
fmt.Fprintf(fl.buf, format, v...)
}

func (fl *fakeLogger) String() string {
return fl.buf.String()
}

var emptyRoundTrip = `SENT To Atlas Request:
<nil>
RECV From Atlas Response:
<nil>
`

func TestRoundTrip(t *testing.T) {
logger := NewFakeLogger()
cl := atlas.NewClientLogger(&fakeRoundTripper{}, logger)

_, err := cl.RoundTrip(nil)

assert.NoError(t, err)
assert.Equal(t, emptyRoundTrip, logger.String())
}

func TestNewDefaultFileLog(t *testing.T) {
assert.Equal(t,
atlas.NewFileLog("./atlas-http.log"),
atlas.NewDefaultFileLog(),
)
}

func TestLogToFile(t *testing.T) {
tmpfile, err := os.CreateTemp("", "log-to-file-test")
assert.NoError(t, err)
tmpfile.Close()
defer os.Remove(tmpfile.Name())
fl := atlas.NewFileLog(tmpfile.Name())

fl.Printf("this is a %s", "test")
contents, err := os.ReadFile(tmpfile.Name())
assert.NoError(t, err)
assert.Equal(t, "this is a test\n", string(contents))
}

func TestLoggerClientBuilder(t *testing.T) {
cl := atlas.NewDefaultClientLogger()
customClientFn := atlas.LoggedClientBuilder(cl)
cc := customClientFn()
assert.Equal(t, cl, cc.Transport)
}
2 changes: 2 additions & 0 deletions test/int/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"k8s.io/apimachinery/pkg/types"

"github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/atlas"
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/connectionsecret"
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/compat"

Expand Down Expand Up @@ -61,6 +62,7 @@ var _ = Describe("AtlasDeployment", Label("int", "AtlasDeployment"), func() {
)

BeforeEach(func() {
atlas.CustomHTTPClientFn = atlas.LoggedClientBuilder(atlas.NewDefaultClientLogger())
prepareControllers()

createdDeployment = &mdbv1.AtlasDeployment{}
Expand Down

0 comments on commit dfe4b1e

Please sign in to comment.