Skip to content

Commit 1fbdd98

Browse files
CLOUDP-60178: Implement logs API, Atlas (#79)
1 parent a1a83bd commit 1fbdd98

File tree

4 files changed

+264
-22
lines changed

4 files changed

+264
-22
lines changed

mongodbatlas/logs.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package mongodbatlas
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
)
9+
10+
const logsPath = "groups/%s/clusters/%s/logs/%s"
11+
12+
// LogsService is an interface for interfacing with the Logs
13+
// endpoints of the MongoDB Atlas API.
14+
// See more: https://docs.atlas.mongodb.com/reference/api/logs/
15+
type LogsService interface {
16+
Get(context.Context, string, string, string, io.Writer, *DateRangetOptions) (*Response, error)
17+
}
18+
19+
// LogsServiceOp handles communication with the Logs related methods of the
20+
// MongoDB Atlas API
21+
type LogsServiceOp struct {
22+
Client GZipRequestDoer
23+
}
24+
25+
// DateRangetOptions specifies an optional date range query.
26+
type DateRangetOptions struct {
27+
StartDate string `url:"startDate,omitempty"`
28+
EndDate string `url:"endDate,omitempty"`
29+
}
30+
31+
// Get gets a compressed (.gz) log file that contains a range of log messages for a particular host.
32+
// Note: The input parameter out (io.Writer) is not closed by this function.
33+
// See more: https://docs.atlas.mongodb.com/reference/api/logs/
34+
func (s *LogsServiceOp) Get(ctx context.Context, groupID string, hostName string, logName string, out io.Writer, opts *DateRangetOptions) (*Response, error) {
35+
if groupID == "" {
36+
return nil, NewArgError("groupID", "must be set")
37+
}
38+
39+
if hostName == "" {
40+
return nil, NewArgError("hostName", "must be set")
41+
}
42+
43+
if logName == "" {
44+
return nil, NewArgError("logName", "must be set")
45+
}
46+
47+
basePath := fmt.Sprintf(logsPath, groupID, hostName, logName)
48+
49+
// Add query params
50+
path, err := setListOptions(basePath, opts)
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
req, err := s.Client.NewGZipRequest(ctx, http.MethodGet, path)
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
resp, err := s.Client.Do(ctx, req, out)
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
return resp, nil
66+
67+
}

mongodbatlas/logs_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package mongodbatlas
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"net/http"
7+
"testing"
8+
)
9+
10+
func TestLogs_Get(t *testing.T) {
11+
setup()
12+
defer teardown()
13+
14+
groupID := "1"
15+
cluster := "test-username"
16+
log := "log"
17+
18+
mux.HandleFunc(fmt.Sprintf("/groups/%s/clusters/%s/logs/%s", groupID, cluster, log), func(w http.ResponseWriter, r *http.Request) {
19+
testMethod(t, r, http.MethodGet)
20+
fmt.Fprint(w, "test")
21+
})
22+
23+
buf := new(bytes.Buffer)
24+
_, err := client.Logs.Get(ctx, groupID, cluster, log, buf, nil)
25+
26+
if buf.String() != "test" {
27+
t.Fatalf("Logs.Get returned error: %v", err)
28+
}
29+
}

mongodbatlas/mongodbatlas.go

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,29 @@ const (
2222
defaultBaseURL = "https://cloud.mongodb.com/api/atlas/v1.0/"
2323
userAgent = "go-mongodbatlas" + libraryVersion
2424
mediaType = "application/json"
25+
gzipMediaType = "application/gzip"
2526
)
2627

27-
type RequestDoer interface {
28-
NewRequest(context.Context, string, string, interface{}) (*http.Request, error)
28+
type Doer interface {
2929
Do(context.Context, *http.Request, interface{}) (*Response, error)
30+
}
31+
32+
type Completer interface {
3033
OnRequestCompleted(RequestCompletionCallback)
3134
}
3235

36+
type RequestDoer interface {
37+
Doer
38+
Completer
39+
NewRequest(context.Context, string, string, interface{}) (*http.Request, error)
40+
}
41+
42+
type GZipRequestDoer interface {
43+
Doer
44+
Completer
45+
NewGZipRequest(context.Context, string, string) (*http.Request, error)
46+
}
47+
3348
// Client manages communication with MongoDBAtlas v1.0 API
3449
type Client struct {
3550
client *http.Client
@@ -71,6 +86,7 @@ type Client struct {
7186
ProcessDiskMeasurements ProcessDiskMeasurementsService
7287
ProcessDatabases ProcessDatabasesService
7388
Indexes IndexesService
89+
Logs LogsService
7490

7591
onRequestCompleted RequestCompletionCallback
7692
}
@@ -195,6 +211,7 @@ func NewClient(httpClient *http.Client) *Client {
195211
c.ProcessDiskMeasurements = &ProcessDiskMeasurementsServiceOp{Client: c}
196212
c.ProcessDatabases = &ProcessDatabasesServiceOp{Client: c}
197213
c.Indexes = &IndexesServiceOp{Client: c}
214+
c.Logs = &LogsServiceOp{Client: c}
198215

199216
return c
200217
}
@@ -239,17 +256,13 @@ func SetUserAgent(ua string) ClientOpt {
239256
// BaseURL of the Client. Relative URLS should always be specified without a preceding slash. If specified, the
240257
// value pointed to by body is JSON encoded and included in as the request body.
241258
func (c *Client) NewRequest(ctx context.Context, method, urlStr string, body interface{}) (*http.Request, error) {
242-
rel, err := url.Parse(urlStr)
259+
u, err := c.BaseURL.Parse(urlStr)
243260
if err != nil {
244261
return nil, err
245262
}
246-
247-
u := c.BaseURL.ResolveReference(rel)
248-
249-
buf := new(bytes.Buffer)
263+
var buf io.Reader
250264
if body != nil {
251-
err = json.NewEncoder(buf).Encode(body)
252-
if err != nil {
265+
if buf, err = c.newEncodedBody(body); err != nil {
253266
return nil, err
254267
}
255268
}
@@ -259,9 +272,44 @@ func (c *Client) NewRequest(ctx context.Context, method, urlStr string, body int
259272
return nil, err
260273
}
261274

262-
req.Header.Add("Content-Type", mediaType)
275+
if body != nil {
276+
req.Header.Set("Content-Type", mediaType)
277+
}
263278
req.Header.Add("Accept", mediaType)
264-
req.Header.Add("User-Agent", c.UserAgent)
279+
if c.UserAgent != "" {
280+
req.Header.Set("User-Agent", c.UserAgent)
281+
}
282+
return req, nil
283+
}
284+
285+
// newEncodedBody returns an ReadWriter object containing the body of the http request
286+
func (c *Client) newEncodedBody(body interface{}) (io.Reader, error) {
287+
buf := &bytes.Buffer{}
288+
enc := json.NewEncoder(buf)
289+
enc.SetEscapeHTML(false)
290+
err := enc.Encode(body)
291+
return buf, err
292+
}
293+
294+
// NewGZipRequest creates an API request that accepts gzip. A relative URL can be provided in urlStr, which will be resolved to the
295+
// BaseURL of the Client. Relative URLS should always be specified without a preceding slash.
296+
func (c *Client) NewGZipRequest(ctx context.Context, method, urlStr string) (*http.Request, error) {
297+
rel, err := url.Parse(urlStr)
298+
if err != nil {
299+
return nil, err
300+
}
301+
302+
u := c.BaseURL.ResolveReference(rel)
303+
304+
req, err := http.NewRequest(method, u.String(), nil)
305+
if err != nil {
306+
return nil, err
307+
}
308+
309+
req.Header.Add("Accept", gzipMediaType)
310+
if c.UserAgent != "" {
311+
req.Header.Set("User-Agent", c.UserAgent)
312+
}
265313
return req, nil
266314
}
267315

@@ -276,6 +324,14 @@ func (c *Client) OnRequestCompleted(rc RequestCompletionCallback) {
276324
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
277325
resp, err := DoRequestWithClient(ctx, c.client, req)
278326
if err != nil {
327+
// If we got an error, and the context has been canceled,
328+
// the context's error is probably more useful.
329+
select {
330+
case <-ctx.Done():
331+
return nil, ctx.Err()
332+
default:
333+
}
334+
279335
return nil, err
280336
}
281337
if c.onRequestCompleted != nil {
@@ -288,7 +344,7 @@ func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Res
288344
}
289345
}()
290346

291-
response := newResponse(resp)
347+
response := &Response{Response: resp}
292348

293349
err = CheckResponse(resp)
294350
if err != nil {
@@ -302,9 +358,12 @@ func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Res
302358
return nil, err
303359
}
304360
} else {
305-
err = json.NewDecoder(resp.Body).Decode(v)
306-
if err != nil {
307-
return nil, err
361+
decErr := json.NewDecoder(resp.Body).Decode(v)
362+
if decErr == io.EOF {
363+
decErr = nil // ignore EOF errors caused by empty response body
364+
}
365+
if decErr != nil {
366+
err = decErr
308367
}
309368
}
310369
}
@@ -338,13 +397,6 @@ func CheckResponse(r *http.Response) error {
338397
return errorResponse
339398
}
340399

341-
// newResponse creates a new Response for the provided http.Response
342-
func newResponse(r *http.Response) *Response {
343-
response := Response{Response: r}
344-
345-
return &response
346-
}
347-
348400
// DoRequestWithClient submits an HTTP request using the specified client.
349401
func DoRequestWithClient(
350402
ctx context.Context,

mongodbatlas/mongodbatlas_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package mongodbatlas
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"io/ioutil"
78
"net/http"
@@ -102,6 +103,23 @@ type testRequestBody struct {
102103
TestData string `json:"testUserData"`
103104
}
104105

106+
// If a nil body is passed to mongodbatlas.NewRequest, make sure that nil is also
107+
// passed to http.NewRequest. In most cases, passing an io.Reader that returns
108+
// no content is fine, since there is no difference between an HTTP request
109+
// body that is an empty string versus one that is not set at all. However in
110+
// certain cases, intermediate systems may treat these differently resulting in
111+
// subtle errors.
112+
func TestNewRequest_emptyBody(t *testing.T) {
113+
c := NewClient(nil)
114+
req, err := c.NewRequest(ctx, http.MethodGet, ".", nil)
115+
if err != nil {
116+
t.Fatalf("NewRequest returned unexpected error: %v", err)
117+
}
118+
if req.Body != nil {
119+
t.Fatalf("constructed request contains a non-nil Body")
120+
}
121+
}
122+
105123
func TestNewRequest_withUserData(t *testing.T) {
106124
c := NewClient(nil)
107125

@@ -153,6 +171,65 @@ func TestNewRequest_withCustomUserAgent(t *testing.T) {
153171
}
154172
}
155173

174+
func TestNewGZipRequest_emptyBody(t *testing.T) {
175+
c := NewClient(nil)
176+
req, err := c.NewGZipRequest(ctx, http.MethodGet, ".")
177+
if err != nil {
178+
t.Fatalf("NewRequest returned unexpected error: %v", err)
179+
}
180+
if req.Body != nil {
181+
t.Fatalf("constructed request contains a non-nil Body")
182+
}
183+
}
184+
185+
func TestNewGZipRequest_withCustomUserAgent(t *testing.T) {
186+
ua := "testing/0.0.1"
187+
c, err := New(nil, SetUserAgent(ua))
188+
189+
if err != nil {
190+
t.Fatalf("New() unexpected error: %v", err)
191+
}
192+
193+
req, _ := c.NewGZipRequest(ctx, http.MethodGet, "/foo")
194+
195+
expected := fmt.Sprintf("%s %s", ua, userAgent)
196+
if got := req.Header.Get("User-Agent"); got != expected {
197+
t.Errorf("New() UserAgent = %s; expected %s", got, expected)
198+
}
199+
}
200+
201+
func TestNewGZipRequest_badURL(t *testing.T) {
202+
c := NewClient(nil)
203+
_, err := c.NewGZipRequest(ctx, http.MethodGet, ":/.")
204+
testURLParseError(t, err)
205+
}
206+
207+
func TestNewGZipRequest(t *testing.T) {
208+
c := NewClient(nil)
209+
210+
requestPath := "foo"
211+
212+
inURL, outURL := requestPath, defaultBaseURL+requestPath
213+
req, _ := c.NewGZipRequest(ctx, http.MethodGet, inURL)
214+
215+
// test relative URL was expanded
216+
if req.URL.String() != outURL {
217+
t.Errorf("NewGZipRequest(%v) URL = %v, expected %v", inURL, req.URL, outURL)
218+
}
219+
220+
// test accept content type is correct
221+
accept := req.Header.Get("Accept")
222+
if gzipMediaType != accept {
223+
t.Errorf("NewGZipRequest() Accept = %v, expected %v", accept, gzipMediaType)
224+
}
225+
226+
// test default user-agent is attached to the request
227+
userAgent := req.Header.Get("User-Agent")
228+
if c.UserAgent != userAgent {
229+
t.Errorf("NewGZipRequest() User-Agent = %v, expected %v", userAgent, c.UserAgent)
230+
}
231+
}
232+
156233
func TestDo(t *testing.T) {
157234
setup()
158235
defer teardown()
@@ -218,6 +295,23 @@ func TestDo_redirectLoop(t *testing.T) {
218295
}
219296
}
220297

298+
func TestDo_noContent(t *testing.T) {
299+
setup()
300+
defer teardown()
301+
302+
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
303+
w.WriteHeader(http.StatusNoContent)
304+
})
305+
306+
var body json.RawMessage
307+
308+
req, _ := client.NewRequest(ctx, http.MethodGet, ".", nil)
309+
_, err := client.Do(context.Background(), req, &body)
310+
if err != nil {
311+
t.Fatalf("Do returned unexpected error: %v", err)
312+
}
313+
}
314+
221315
func TestCheckResponse(t *testing.T) {
222316
res := &http.Response{
223317
Request: &http.Request{},

0 commit comments

Comments
 (0)