Repository: newrelic/newrelic-client-go
Pull request #438 (2020-08-25 19:54:35)

feat(logs): Implement Log API
Cleaned-up based on the previous comments and fixed the commit message.


Patch:
diff --git a/go.sum b/go.sum
index 0eaa4e8..c4aef32 100644
--- a/go.sum
+++ b/go.sum
@@ -721,6 +721,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
 github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
 github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
 github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
+github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
 github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
 github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
diff --git a/internal/http/auth.go b/internal/http/auth.go
index dcaa1c0..d342a1b 100644
--- a/internal/http/auth.go
+++ b/internal/http/auth.go
@@ -44,3 +44,9 @@ type InsightsInsertKeyAuthorizer struct{}
 func (a *InsightsInsertKeyAuthorizer) AuthorizeRequest(r *Request, c *config.Config) {
 	r.SetHeader("X-Insert-Key", c.InsightsInsertKey)
 }
+
+type LicenseKeyAuthorizer struct{}
+
+func (a *LicenseKeyAuthorizer) AuthorizeRequest(r *Request, c *config.Config) {
+	r.SetHeader("X-License-Key", c.LicenseKey)
+}
diff --git a/newrelic/newrelic.go b/newrelic/newrelic.go
index 45e120f..1927552 100644
--- a/newrelic/newrelic.go
+++ b/newrelic/newrelic.go
@@ -18,6 +18,7 @@ import (
 	"github.com/newrelic/newrelic-client-go/pkg/entities"
 	"github.com/newrelic/newrelic-client-go/pkg/events"
 	"github.com/newrelic/newrelic-client-go/pkg/eventstometrics"
+	"github.com/newrelic/newrelic-client-go/pkg/logs"
 	"github.com/newrelic/newrelic-client-go/pkg/nerdgraph"
 	"github.com/newrelic/newrelic-client-go/pkg/nerdstorage"
 	"github.com/newrelic/newrelic-client-go/pkg/nrdb"
@@ -38,6 +39,7 @@ type NewRelic struct {
 	Entities        entities.Entities
 	Events          events.Events
 	EventsToMetrics eventstometrics.EventsToMetrics
+	Logs            logs.Logs
 	NerdGraph       nerdgraph.NerdGraph
 	NerdStorage     nerdstorage.NerdStorage
 	Nrdb            nrdb.Nrdb
@@ -77,6 +79,7 @@ func New(opts ...ConfigOption) (*NewRelic, error) {
 		Entities:        entities.New(config),
 		Events:          events.New(config),
 		EventsToMetrics: eventstometrics.New(config),
+		Logs:            logs.New(config),
 		NerdGraph:       nerdgraph.New(config),
 		NerdStorage:     nerdstorage.New(config),
 		Nrdb:            nrdb.New(config),
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 88e9cda..90b6b61 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -12,6 +12,9 @@ import (
 
 // Config contains all the configuration data for the API Client.
 type Config struct {
+	// LicenseKey to authenticate Log API requests
+	// see: https://docs.newrelic.com/docs/accounts/accounts-billing/account-setup/new-relic-license-key
+	LicenseKey string
 	// PersonalAPIKey to authenticate API requests
 	// see: https://docs.newrelic.com/docs/apis/get-started/intro-apis/types-new-relic-api-keys#personal-api-key
 	PersonalAPIKey string
diff --git a/pkg/logs/logs.go b/pkg/logs/logs.go
new file mode 100644
index 0000000..ac206b0
--- /dev/null
+++ b/pkg/logs/logs.go
@@ -0,0 +1,53 @@
+package logs
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/newrelic/newrelic-client-go/internal/http"
+	"github.com/newrelic/newrelic-client-go/internal/logging"
+	"github.com/newrelic/newrelic-client-go/pkg/config"
+)
+
+// Logs is used to send log data to the New Relic Log API
+type Logs struct {
+	client http.Client
+	config config.Config
+	logger logging.Logger
+}
+
+// New is used to create a new Logs client instance.
+func New(cfg config.Config) Logs {
+	cfg.Compression = config.Compression.Gzip
+
+	client := http.NewClient(cfg)
+	client.SetAuthStrategy(&http.LicenseKeyAuthorizer{})
+
+	pkg := Logs{
+		client: client,
+		config: cfg,
+		logger: cfg.GetLogger(),
+	}
+
+	return pkg
+}
+
+// CreateLogEntry reports a log entry to New Relic.
+// It's up to the caller to send a valid Log API payload, no checking done here
+func (l *Logs) CreateLogEntry(logEntry interface{}) error {
+	if logEntry == nil {
+		return errors.New("logs: CreateLogEntry: logEntry is nil, nothing to do")
+	}
+
+	rsp, err := l.client.Post(l.config.Region().LogsURL(), nil, logEntry, nil)
+
+	if err != nil {
+		return err
+	}
+
+	if !(rsp.StatusCode < 299) {
+		return fmt.Errorf("failed creating log entry: %v", rsp)
+	}
+
+	return nil
+}
diff --git a/pkg/region/region.go b/pkg/region/region.go
index eb489fd..a7e013d 100644
--- a/pkg/region/region.go
+++ b/pkg/region/region.go
@@ -19,11 +19,12 @@ type Name string
 // Region represents the members of the Region enumeration.
 type Region struct {
 	name                  string
-	restBaseURL           string
 	infrastructureBaseURL string
-	syntheticsBaseURL     string
-	nerdGraphBaseURL      string
 	insightsBaseURL       string
+	logsBaseURL           string
+	nerdGraphBaseURL      string
+	restBaseURL           string
+	syntheticsBaseURL     string
 }
 
 // String returns a human readable value for the specified Region Name
@@ -187,3 +188,24 @@ func concatURLPaths(host string, path []string) (string, error) {
 
 	return strings.Join(elements, "/"), nil
 }
+
+//
+// Logs
+//
+
+// SetLogsBaseURL Allows overriding the Logs Base URL
+func (r *Region) SetLogsBaseURL(url string) {
+	if r != nil && url != "" {
+		r.logsBaseURL = url
+	}
+}
+
+// LogsURL returns the Full URL for the Log API
+func (r *Region) LogsURL() string {
+	if r == nil {
+		log.Errorf("call to nil region.LogsURL")
+		return ""
+	}
+
+	return r.logsBaseURL
+}
diff --git a/pkg/region/region_constants.go b/pkg/region/region_constants.go
index e1d8607..e66dad8 100644
--- a/pkg/region/region_constants.go
+++ b/pkg/region/region_constants.go
@@ -21,6 +21,7 @@ var Regions = map[Name]*Region{
 		name:                  "US",
 		infrastructureBaseURL: "https://infra-api.newrelic.com/v2",
 		insightsBaseURL:       "https://insights-collector.newrelic.com/v1",
+		logsBaseURL:           "https://log-api.newrelic.com/log/v1",
 		nerdGraphBaseURL:      "https://api.newrelic.com/graphql",
 		restBaseURL:           "https://api.newrelic.com/v2",
 		syntheticsBaseURL:     "https://synthetics.newrelic.com/synthetics/api",
@@ -29,6 +30,7 @@ var Regions = map[Name]*Region{
 		name:                  "EU",
 		infrastructureBaseURL: "https://infra-api.eu.newrelic.com/v2",
 		insightsBaseURL:       "https://insights-collector.eu.newrelic.com/v1",
+		logsBaseURL:           "https://log-api.eu.newrelic.com/log/v1",
 		nerdGraphBaseURL:      "https://api.eu.newrelic.com/graphql",
 		restBaseURL:           "https://api.eu.newrelic.com/v2",
 		syntheticsBaseURL:     "https://synthetics.eu.newrelic.com/synthetics/api",
@@ -37,6 +39,7 @@ var Regions = map[Name]*Region{
 		name:                  "Staging",
 		infrastructureBaseURL: "https://staging-infra-api.newrelic.com/v2",
 		insightsBaseURL:       "https://staging-insights-collector.newrelic.com/v1",
+		logsBaseURL:           "https://staging-log-api.newrelic.com/log/v1",
 		nerdGraphBaseURL:      "https://staging-api.newrelic.com/graphql",
 		restBaseURL:           "https://staging-api.newrelic.com/v2",
 		syntheticsBaseURL:     "https://staging-synthetics.newrelic.com/synthetics/api",


Test patch:
diff --git a/pkg/logs/example_basic_test.go b/pkg/logs/example_basic_test.go
new file mode 100644
index 0000000..30a6d6e
--- /dev/null
+++ b/pkg/logs/example_basic_test.go
@@ -0,0 +1,34 @@
+// +build integration
+
+package logs
+
+import (
+	"log"
+	"os"
+
+	"github.com/newrelic/newrelic-client-go/pkg/config"
+)
+
+//func TestExample_basic(t *testing.T) {
+func Example_basic() {
+	// Initialize the client configuration.  A New Relic License Key is required
+	// to communicate with the backend API.
+	cfg := config.New()
+	cfg.LicenseKey = os.Getenv("NEW_RELIC_LICENSE_KEY")
+	cfg.LogLevel = "trace"
+	cfg.Compression = config.Compression.None
+
+	// Initialize the client.
+	client := New(cfg)
+
+	logEntry := struct {
+		Message string `json:"message"`
+	}{
+		Message: "INFO: From example basic test.go",
+	}
+
+	// Post a Log entry
+	if err := client.CreateLogEntry(logEntry); err != nil {
+		log.Fatal("error posting Log entry: ", err)
+	}
+}
diff --git a/pkg/logs/logs_integration_test.go b/pkg/logs/logs_integration_test.go
new file mode 100644
index 0000000..4589f1d
--- /dev/null
+++ b/pkg/logs/logs_integration_test.go
@@ -0,0 +1,51 @@
+// +build integration
+
+// Requires NEW_RELIC_LICENSE_KEY envvar (APM License Key)
+
+package logs
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	nr "github.com/newrelic/newrelic-client-go/pkg/testhelpers"
+)
+
+type testDatum struct {
+	Datum interface{}
+	err   error
+}
+
+var testData = []testDatum{
+	{
+		Datum: struct {
+			Message string `json:"message"`
+		}{
+			Message: "INFO: simple message test",
+		},
+		err: nil,
+	},
+}
+
+// Test: CreateEvent
+func TestIntegrationEvents(t *testing.T) {
+	t.Parallel()
+
+	client := newIntegrationTestClient(t)
+
+	for _, test := range testData {
+		err := client.CreateLogEntry(test.Datum)
+		if test.err == nil {
+			assert.NoError(t, err)
+		} else {
+			assert.Equal(t, test.err, err)
+		}
+	}
+}
+
+func newIntegrationTestClient(t *testing.T) Logs {
+	tc := nr.NewIntegrationTestConfig(t)
+
+	return New(tc)
+}
diff --git a/pkg/region/region_test.go b/pkg/region/region_test.go
index 322d747..8b1fef4 100644
--- a/pkg/region/region_test.go
+++ b/pkg/region/region_test.go
@@ -109,6 +109,20 @@ func TestSyntheticsURLs(t *testing.T) {
 	}
 }
 
+func TestLogsURLs(t *testing.T) {
+	t.Parallel()
+
+	pairs := map[Name]string{
+		US:      "https://log-api.newrelic.com/log/v1",
+		EU:      "https://log-api.eu.newrelic.com/log/v1",
+		Staging: "https://staging-log-api.newrelic.com/log/v1",
+	}
+
+	for k, v := range pairs {
+		assert.Equal(t, v, Regions[k].LogsURL())
+	}
+}
+
 func TestConcatURLPaths(t *testing.T) {
 	t.Parallel()
 
diff --git a/pkg/testhelpers/config.go b/pkg/testhelpers/config.go
index 5ddf66a..019c6eb 100644
--- a/pkg/testhelpers/config.go
+++ b/pkg/testhelpers/config.go
@@ -16,6 +16,7 @@ const (
 	LogLevel       = "debug"                                           // LogLevel used in mock configs
 	PersonalAPIKey = "personalAPIKey"                                  // PersonalAPIKey used in mock configs (from Environment for Integration tests)
 	UserAgent      = "newrelic/newrelic-client-go (automated testing)" // UserAgent used in mock configs
+	LicenseKey     = "APMLicenseKey"                                   // LicenseKey used in mock configs
 )
 
 // NewTestConfig returns a fully saturated configration with modified BaseURLs
@@ -28,12 +29,14 @@ func NewTestConfig(t *testing.T, testServer *httptest.Server) config.Config {
 	cfg.LogLevel = LogLevel
 	cfg.PersonalAPIKey = PersonalAPIKey
 	cfg.UserAgent = UserAgent
+	cfg.LicenseKey = LicenseKey
 
 	if testServer != nil {
 		cfg.Region().SetInfrastructureBaseURL(testServer.URL)
 		cfg.Region().SetNerdGraphBaseURL(testServer.URL)
 		cfg.Region().SetRestBaseURL(testServer.URL)
 		cfg.Region().SetSyntheticsBaseURL(testServer.URL)
+		cfg.Region().SetLogsBaseURL(testServer.URL)
 	}
 
 	return cfg
@@ -45,6 +48,7 @@ func NewIntegrationTestConfig(t *testing.T) config.Config {
 	envPersonalAPIKey := os.Getenv("NEW_RELIC_API_KEY")
 	envAdminAPIKey := os.Getenv("NEW_RELIC_ADMIN_API_KEY")
 	envInsightsInsertKey := os.Getenv("NEW_RELIC_INSIGHTS_INSERT_KEY")
+	envLicenseKey := os.Getenv("NEW_RELIC_LICENSE_KEY")
 	envRegion := os.Getenv("NEW_RELIC_REGION")
 
 	if envPersonalAPIKey == "" && envAdminAPIKey == "" {
@@ -60,6 +64,7 @@ func NewIntegrationTestConfig(t *testing.T) config.Config {
 	cfg.PersonalAPIKey = envPersonalAPIKey
 	cfg.AdminAPIKey = envAdminAPIKey
 	cfg.InsightsInsertKey = envInsightsInsertKey
+	cfg.LicenseKey = envLicenseKey
 
 	if envRegion != "" {
 		regName, err := region.Parse(envRegion)