Repository: newrelic/newrelic-client-go
Pull request #1293 (2025-04-22 08:58:11)

feat: Add Metrics Client
This PR is used to add metrics client to go client library. The changes include:
1. takes NR region as parameter for selecting appropriate metrics endpoint.
2. retry is supported as part of base client itself. 
3. added gzip compression
4. error handling is part of the base client itself

Ran unit tests and integration tests for the new client. Refer to pkg/metrics coverage:
<img width="515" alt="Screenshot 2025-04-28 at 10 50 42 AM" src="https://github.com/user-attachments/assets/bde73a46-f3dc-4fa8-bb2f-13287d12b8ae" />

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/newrelic/newrelic-client-go?pullRequest=1293) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/newrelic/newrelic-client-go?pullRequest=1293) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/newrelic/newrelic-client-go?pullRequest=1293) it.</sub>
CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/newrelic/newrelic-client-go?pullRequest=1293) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/newrelic/newrelic-client-go?pullRequest=1293) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/newrelic/newrelic-client-go?pullRequest=1293) it.</sub>
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/newrelic/newrelic-client-go/pull/1293?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=newrelic) Report
Attention: Patch coverage is `2.70270%` with `36 lines` in your changes missing coverage. Please review.
> Project coverage is 35.01%. Comparing base [(`fb77e6f`)](https://app.codecov.io/gh/newrelic/newrelic-client-go/commit/fb77e6fdd165f2d0790a4d1a66e4a8e9b6dc28d9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=newrelic) to head [(`417bcbb`)](https://app.codecov.io/gh/newrelic/newrelic-client-go/commit/417bcbb4776937daa7faefd280dc7840f24f34bd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=newrelic).

| [Files with missing lines](https://app.codecov.io/gh/newrelic/newrelic-client-go/pull/1293?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=newrelic) | Patch % | Lines |
|---|---|---|
| [pkg/metrics/metrics.go](https://app.codecov.io/gh/newrelic/newrelic-client-go/pull/1293?src=pr&el=tree&filepath=pkg%2Fmetrics%2Fmetrics.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=newrelic#diff-cGtnL21ldHJpY3MvbWV0cmljcy5nbw==) | 0.00% | [23 Missing :warning: ](https://app.codecov.io/gh/newrelic/newrelic-client-go/pull/1293?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=newrelic) |
| [pkg/region/region.go](https://app.codecov.io/gh/newrelic/newrelic-client-go/pull/1293?src=pr&el=tree&filepath=pkg%2Fregion%2Fregion.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=newrelic#diff-cGtnL3JlZ2lvbi9yZWdpb24uZ28=) | 0.00% | [10 Missing :warning: ](https://app.codecov.io/gh/newrelic/newrelic-client-go/pull/1293?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=newrelic) |
| [pkg/region/region\_constants.go](https://app.codecov.io/gh/newrelic/newrelic-client-go/pull/1293?src=pr&el=tree&filepath=pkg%2Fregion%2Fregion_constants.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=newrelic#diff-cGtnL3JlZ2lvbi9yZWdpb25fY29uc3RhbnRzLmdv) | 0.00% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/newrelic/newrelic-client-go/pull/1293?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=newrelic) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1293      +/-   ##
==========================================
- Coverage   35.12%   35.01%   -0.11%     
==========================================
  Files         127      128       +1     
  Lines        7659     7696      +37     
==========================================
+ Hits         2690     2695       +5     
- Misses       4786     4819      +33     
+ Partials      183      182       -1     
```

| [Flag](https://app.codecov.io/gh/newrelic/newrelic-client-go/pull/1293/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=newrelic) | Coverage Δ | |
|---|---|---|
| [unit](https://app.codecov.io/gh/newrelic/newrelic-client-go/pull/1293/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=newrelic) | `35.01% <2.70%> (-0.11%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=newrelic#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/newrelic/newrelic-client-go/pull/1293?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=newrelic).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=newrelic).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>

Patch:
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
new file mode 100644
index 0000000..536d244
--- /dev/null
+++ b/pkg/metrics/metrics.go
@@ -0,0 +1,47 @@
+package metrics
+
+import (
+	"errors"
+	"github.com/newrelic/newrelic-client-go/v2/internal/http"
+	"github.com/newrelic/newrelic-client-go/v2/pkg/config"
+	"github.com/newrelic/newrelic-client-go/v2/pkg/logging"
+)
+
+// Metrics is used to send metric data to the New Relic Metrics API
+type Metrics struct {
+	client http.Client
+	config config.Config
+	logger logging.Logger
+}
+
+// New is used to create a new Metrics client instance.
+func New(cfg config.Config) Metrics {
+	cfg.Compression = config.Compression.Gzip
+	client := http.NewClient(cfg)
+	client.SetAuthStrategy(&http.LicenseKeyAuthorizer{})
+
+	pkg := Metrics{
+		client: client,
+		config: cfg,
+		logger: cfg.GetLogger(),
+	}
+
+	return pkg
+}
+
+// CreateMetricEntry reports a metric entry to New Relic.
+// It's up to the caller to send a valid Metric API payload, no checking done here
+func (m *Metrics) CreateMetricEntry(metricEntry any) error {
+	if metricEntry == nil {
+		return errors.New("metrics: CreateMetricEntry: metricEntry is nil, nothing to do")
+	}
+	_, err := m.client.Post(m.config.Region().MetricsURL(), nil, metricEntry, nil)
+
+	// If no error is returned then the call succeeded
+	if err != nil {
+		m.logger.Error("metrics: Error: CreateMetricEntry: %s", err.Error())
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/region/region.go b/pkg/region/region.go
index d9c6018..2ca873d 100644
--- a/pkg/region/region.go
+++ b/pkg/region/region.go
@@ -26,6 +26,7 @@ type Region struct {
 	restBaseURL           string
 	syntheticsBaseURL     string
 	insightsKeysBaseURL   string
+	metricsBaseURL        string
 }
 
 // String returns a human readable value for the specified Region Name
@@ -234,3 +235,24 @@ func (r *Region) LogsURL() string {
 
 	return r.logsBaseURL
 }
+
+//
+// Metrics
+//
+
+// SetMetricsBaseURL Allows overriding the Metrics Base URL
+func (r *Region) SetMetricsBaseURL(url string) {
+	if r != nil && url != "" {
+		r.metricsBaseURL = url
+	}
+}
+
+// MetricsURL returns the Full URL for the Metrics API
+func (r *Region) MetricsURL() string {
+	if r == nil {
+		log.Errorf("call to nil region.MetricsURL")
+		return ""
+	}
+
+	return r.metricsBaseURL
+}
diff --git a/pkg/region/region_constants.go b/pkg/region/region_constants.go
index a4d2737..769aab4 100644
--- a/pkg/region/region_constants.go
+++ b/pkg/region/region_constants.go
@@ -31,6 +31,7 @@ var Regions = map[Name]*Region{
 		nerdGraphBaseURL:      "https://api.newrelic.com/graphql",
 		restBaseURL:           "https://api.newrelic.com/v2",
 		syntheticsBaseURL:     "https://synthetics.newrelic.com/synthetics/api",
+		metricsBaseURL:        "https://metric-api.newrelic.com/metric/v1",
 	},
 	EU: {
 		name:                  "EU",
@@ -41,6 +42,7 @@ var Regions = map[Name]*Region{
 		nerdGraphBaseURL:      "https://api.eu.newrelic.com/graphql",
 		restBaseURL:           "https://api.eu.newrelic.com/v2",
 		syntheticsBaseURL:     "https://synthetics.eu.newrelic.com/synthetics/api",
+		metricsBaseURL:        "https://metric-api.eu.newrelic.com/metric/v1",
 	},
 	Staging: {
 		name:                  "Staging",
@@ -51,6 +53,7 @@ var Regions = map[Name]*Region{
 		nerdGraphBaseURL:      "https://staging-api.newrelic.com/graphql",
 		restBaseURL:           "https://staging-api.newrelic.com/v2",
 		syntheticsBaseURL:     "https://staging-synthetics.newrelic.com/synthetics/api",
+		metricsBaseURL:        "https://staging-metric-api.newrelic.com/metric/v1",
 	},
 	Local: {
 		name:                  "Local",
@@ -61,6 +64,7 @@ var Regions = map[Name]*Region{
 		nerdGraphBaseURL:      "http://localhost:3000/graphql",
 		restBaseURL:           "http://localhost:3000/v2",
 		syntheticsBaseURL:     "http://localhost:3000/synthetics/api",
+		metricsBaseURL:        "http://localhost:3000/metric/v1",
 	},
 }
 
@@ -107,6 +111,9 @@ func Get(r Name) (*Region, error) {
 		if val := os.Getenv("NEW_RELIC_SYNTHETICS_BASE_URL"); val != "" {
 			ret.syntheticsBaseURL = val
 		}
+		if val := os.Getenv("NEW_RELIC_METRICS_BASE_URL"); val != "" {
+			ret.metricsBaseURL = val
+		}
 		return &ret, nil
 	}
 


Test patch:
diff --git a/pkg/metrics/metrics_integration_test.go b/pkg/metrics/metrics_integration_test.go
new file mode 100644
index 0000000..ccf91d7
--- /dev/null
+++ b/pkg/metrics/metrics_integration_test.go
@@ -0,0 +1,59 @@
+//go:build integration
+// +build integration
+
+// Requires NEW_RELIC_LICENSE_KEY env var
+
+package metrics
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	nr "github.com/newrelic/newrelic-client-go/v2/pkg/testhelpers"
+)
+
+type testDatum struct {
+	Datum interface{}
+	err   error
+}
+
+var testData = []testDatum{
+	{
+		Datum: struct {
+			Name       string            `json:"name"`
+			Type       string            `json:"type"`
+			Value      int               `json:"value"`
+			Attributes map[string]string `json:"attributes"`
+		}{
+			Name:  "service.errors.all",
+			Type:  "gauge",
+			Value: 9,
+			Attributes: map[string]string{
+				"service.response.statuscode": "400",
+			},
+		},
+		err: nil,
+	},
+}
+
+func TestIntegrationMetrics(t *testing.T) {
+	t.Parallel()
+
+	client := newIntegrationTestClient(t)
+
+	for _, test := range testData {
+		err := client.CreateMetricEntry(test.Datum)
+		if test.err == nil {
+			assert.NoError(t, err)
+		} else {
+			assert.Equal(t, test.err, err)
+		}
+	}
+}
+
+func newIntegrationTestClient(t *testing.T) Metrics {
+	tc := nr.NewIntegrationTestConfig(t)
+
+	return New(tc)
+}
diff --git a/pkg/metrics/metrics_unit_test.go b/pkg/metrics/metrics_unit_test.go
new file mode 100644
index 0000000..82d8e07
--- /dev/null
+++ b/pkg/metrics/metrics_unit_test.go
@@ -0,0 +1,58 @@
+//go:build unit
+// +build unit
+
+package metrics
+
+import (
+	mock "github.com/newrelic/newrelic-client-go/v2/pkg/testhelpers"
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var (
+	testMetrics = `{
+		"name": "service.errors.all",
+		"type": "gauge",
+		"value": 9,
+		"attributes": {
+			"service.response.statuscode": "400"
+		}
+	}`
+)
+
+func getMockMetricsResponse(t *testing.T, mockJSONResponse string, statusCode int) Metrics {
+	mockServer := mock.NewMockServer(t, mockJSONResponse, statusCode)
+	testConfig := mock.NewTestConfig(t, mockServer)
+
+	return New(testConfig)
+}
+
+func TestCreateMetricEntry(t *testing.T) {
+	t.Parallel()
+
+	// Arrange
+	metricsClient := getMockMetricsResponse(t, ``, http.StatusAccepted)
+
+	// Act
+	err := metricsClient.CreateMetricEntry(testMetrics)
+
+	// Assert successful metrics call
+	assert.NoError(t, err)
+
+}
+
+func TestNilMetricEntry_FailsWithError(t *testing.T) {
+	t.Parallel()
+
+	// Arrange
+	metricsClient := getMockMetricsResponse(t, ``, http.StatusAccepted)
+
+	// Act
+	err := metricsClient.CreateMetricEntry(nil)
+
+	// Assert that an error is returned
+	assert.Error(t, err)
+	assert.Equal(t, "metrics: CreateMetricEntry: metricEntry is nil, nothing to do", err.Error())
+}
diff --git a/pkg/testhelpers/config.go b/pkg/testhelpers/config.go
index 5985811..4b9a3b7 100644
--- a/pkg/testhelpers/config.go
+++ b/pkg/testhelpers/config.go
@@ -38,6 +38,7 @@ func NewTestConfig(t *testing.T, testServer *httptest.Server) config.Config {
 		cfg.Region().SetRestBaseURL(testServer.URL)
 		cfg.Region().SetSyntheticsBaseURL(testServer.URL)
 		cfg.Region().SetLogsBaseURL(testServer.URL)
+		cfg.Region().SetMetricsBaseURL(testServer.URL)
 	}
 
 	return cfg