diff --git a/.gitignore b/.gitignore index a1338d6..de178f8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ + +# IDE +.idea diff --git a/README.md b/README.md index 12f93f8..80fbdb9 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,20 @@ $ go get github.com/ushios/sumoll # Usage -### Send string to http source collection +## Send string to http source collection ```go client := NewHTTPSourceClient("http://collectors.au.sumologic.com/receiver/v1/http/...") client.Send(strings.NewReader("your message here.")) ``` + +# Developing + +## Integration test + +There's a test actually injecting data to sumologic. In order that to run, you need to set the env variable +`SUMOLL_TEST_HTTP_SOURCE_URL` like this: + +```bash +SUMOLL_TEST_HTTP_SOURCE_URL="https://collectors.eu.sumologic.com/receiver/v1/http/randomCollectorURL" go test --count=1 . -v +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..650d285 --- /dev/null +++ b/go.mod @@ -0,0 +1 @@ +module github.com/dcaba/sumoll diff --git a/http.go b/http.go index e186062..34b6391 100644 --- a/http.go +++ b/http.go @@ -2,28 +2,77 @@ package sumoll import ( "context" + "errors" + "fmt" "io" "net/http" "net/url" "time" ) +type httpClient interface { + Do(req *http.Request) (*http.Response, error) +} + type ( // HTTPSourceClient send to Resource HTTP HTTPSourceClient struct { url *url.URL - client *http.Client + client httpClient UserAgent string + headers *http.Header } + + // HTTPSourceClientOptFunc set some values to client + HTTPSourceClientOptFunc func(*HTTPSourceClient) error ) +// SetXSumoCategoryHeader set X-Sumo-Category's value to header +func SetXSumoCategoryHeader(category string) HTTPSourceClientOptFunc { + return func(h *HTTPSourceClient) error { + if category != "" { + h.headers.Add("X-Sumo-Category", category) + } + return nil + } +} + +// SetXSumoNameHeader set X-Sumo-Name's value to header +func SetXSumoNameHeader(name string) HTTPSourceClientOptFunc { + return func(h *HTTPSourceClient) error { + if name != "" { + h.headers.Add("X-Sumo-Name", name) + } + return nil + } +} + +// SetXSumoHostHeader set X-Sumo-Host's value to header +func SetXSumoHostHeader(host string) HTTPSourceClientOptFunc { + return func(h *HTTPSourceClient) error { + if host != "" { + h.headers.Add("X-Sumo-Host", host) + } + return nil + } +} + // NewHTTPSourceClient create HTTPSourceClient object -func NewHTTPSourceClient(url *url.URL) *HTTPSourceClient { - return &HTTPSourceClient{ +func NewHTTPSourceClient(url *url.URL, opts ...HTTPSourceClientOptFunc) (*HTTPSourceClient, error) { + c := &HTTPSourceClient{ url: url, client: &http.Client{}, UserAgent: UserAgent(), + headers: &http.Header{}, + } + + for _, opts := range opts { + if err := opts(c); err != nil { + return nil, err + } } + + return c, nil } // Send object to sumologic @@ -36,13 +85,37 @@ func (h *HTTPSourceClient) Send(body io.Reader) error { return err } - if _, err := h.client.Do(req); err != nil { + if h.headers != nil { + mergeHeaders(&req.Header, h.headers) + } + + res, err := h.client.Do(req) + if err != nil { return err } + if !validResponseStatus(res.StatusCode) { + return errors.New(fmt.Sprintf("Unexpected response code from Sumologic: %v", res.StatusCode)) + } + return nil } +func validResponseStatus(status int) bool { + return status >= http.StatusOK && status < http.StatusMultipleChoices +} + +func mergeHeaders(merged, input *http.Header) { + if input == nil { + return + } + for k, v := range *input { + if len(v) > 0 { + merged.Add(k, v[0]) + } + } +} + func (h *HTTPSourceClient) newRequest(ctx context.Context, method string, body io.Reader) (*http.Request, error) { u := *h.url diff --git a/http_test.go b/http_test.go index 40fa617..7687bf5 100644 --- a/http_test.go +++ b/http_test.go @@ -1,28 +1,26 @@ package sumoll import ( + "io" + "log" + "net/http" "net/url" "os" "strings" "testing" ) +const os_env_http_source = "SUMOLL_TEST_HTTP_SOURCE_URL" + var ( - httpSourceURL = os.Getenv("SUMOLL_TEST_HTTP_SOURCE_URL") + httpSourceURL = os.Getenv(os_env_http_source) ) -func httpSourceTestAvailable() bool { +func TestHTTPSourceClientSendIntegration(t *testing.T) { if httpSourceURL == "" { - return false - } - - return true -} - -func TestHTTPSourceClientSend(t *testing.T) { - if !httpSourceTestAvailable() { - t.Skip("SUMOLL_TEST_HTTP_SOURCE_URL is not set") + t.Skip(os_env_http_source, "is not set. Value:", httpSourceURL) } + t.Log(os_env_http_source, "is set. Executing integration test") table := []struct { body string @@ -35,8 +33,94 @@ func TestHTTPSourceClientSend(t *testing.T) { if err != nil { t.Fatalf("url.Parse(%s) got error: %s", httpSourceURL, err) } - c := NewHTTPSourceClient(u) + c, err := NewHTTPSourceClient(u) + if err != nil { + t.Fatalf("NewHTTPSourceClient got error") + } + + err = c.Send(strings.NewReader(row.body)) + if err != nil { + t.Error("Error response when sending", row.body, "to", u, ". Err:", err) + } + } +} + +type httpClientMock struct { + checkCategoryValue, checkHostnameValue, checkSourcenameValue string +} + +type nopCloser struct { + io.Reader +} + +func (nopCloser) Close() error { return nil } + +const urlForUnitTests = "https://localEndpoint/receiver/v1/http/myUniqueID" + +func (hc httpClientMock) Do(req *http.Request) (*http.Response, error) { + operation := req.Method + requestSize := req.ContentLength + log.Printf("Mock intercepted %s request against %s with a body of %d bytes", operation, req.Host, requestSize) + responseToSend := http.StatusBadRequest + switch operation { + case "POST": + expectedURL, _ := url.Parse(urlForUnitTests) + if req.URL == nil || *req.URL != *expectedURL { + log.Println("Request against mock did not match the expected URL", expectedURL) + } else if hc.checkCategoryValue != "" && !hasHeaderWithValue(req, "X-Sumo-Category", hc.checkCategoryValue) { + log.Println("Request against mock did not have the expected category value", hc.checkCategoryValue) + } else if hc.checkHostnameValue != "" && !hasHeaderWithValue(req, "X-Sumo-Host", hc.checkHostnameValue) { + log.Println("Request against mock did not have the expected hostname value", hc.checkHostnameValue) + } else if hc.checkSourcenameValue != "" && !hasHeaderWithValue(req, "X-Sumo-Name", hc.checkSourcenameValue) { + log.Println("Request against mock did not have the expected sourcename value", hc.checkSourcenameValue) + } else { + log.Println("All mock tests were successful") + responseToSend = http.StatusOK + } + } + return &http.Response{ + Body: nopCloser{}, + StatusCode: responseToSend, + Status: http.StatusText(responseToSend), + }, nil +} +func hasHeaderWithValue(request *http.Request, header string, expectedValue string) bool { + return request.Header.Get(header) == expectedValue +} + +func TestHTTPSourceClientSendUnit(t *testing.T) { + table := []struct { + category, hostname, sourcename string + }{ + {"", "", ""}, + {"testCategory", "", ""}, + {"", "testHostname", ""}, + {"", "", "testSourcename"}, + {"testCategory", "testHostname", "testSourcename"}, + } + + for _, values := range table { + localUrl, _ := url.Parse(urlForUnitTests) + c, err := NewHTTPSourceClient(localUrl, + SetXSumoCategoryHeader(values.category), + SetXSumoHostHeader(values.hostname), + SetXSumoNameHeader(values.sourcename), + ) + if err != nil { + t.Fatalf("NewHTTPSourceClient got error: %s", err) + } + + c.client = &httpClientMock{ + checkCategoryValue: values.category, + checkHostnameValue: values.hostname, + checkSourcenameValue: values.sourcename, + } + err = c.Send(strings.NewReader("hogehoge")) + if err != nil { + t.Error("Error response when sending payload to", localUrl, + "with", values.category, values.hostname, values.sourcename, + ". Err:", err) + } - c.Send(strings.NewReader(row.body)) } }