Repository: influxdata/influxdb-client-go (go)

Issue:
Allow overriding the default User-Agent header
__Proposal:__
Add a feature allowing users of this client library to override the default `influxdb-client-go` User-Agent HTTP header. This would make it easier to understand which apps are making which requests. E.g., currently, if App1 and App2 use this client, the UA coming from both will read `influxdb-client-go`. Allowing the apps to override the UA would make it easier to distinguish between the two.

__Current behavior:__
User-Agent header is hard coded to `influxdb-client-go`

__Desired behavior:__
Add 2 options for users:
1. Add method to set a field or two (AppName and Version?) for their User-Agent. The client would then construct a User-Agent. Something like `influxdb-client-go/2.3 AppName/Version`. Mark this in the docs as **recommended**.
2. Add a second method that allows completely overriding the User-Agent.

Pull request description:
feat: Allow setting of application name to the User-Agent header value
Closes #356 

## Proposed Changes

Added new option for setting the application name. Application name is then added to the end of Go client version string: E.g. `Influxdb-client-go/2.10.0 (linux; amd64) IoTHub/1.10`
- Added also example how to completely overwrite User-Agent header value using the Doer interface 

## Checklist

<!-- Checkboxes below this note can be erased if not applicable to your Pull Request. -->

- [X] CHANGELOG.md updated
- [X] Rebased/mergeable
- [X] A test has been added if appropriate
- [X] Tests pass
- [X] Commit messages are in [semantic format](https://seesparkbox.com/foundry/semantic_commit_messages)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e093f2..91588e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,12 @@
 ## [unreleased]
+### Features
+- [#358](https://github.com/influxdata/influxdb-client-go/pull/358):
+    - Added possibility to set an application name, which will be part of the User-Agent HTTP header:
+        - Set using `Options.SetApplicationName`
+        - Warning message is written to log if an application name is not set 
+          - This may change to be logged as an error in a future release
+    - Added example how to fully override `User-Agent` header using `Doer` interface
+
 ### Bug fixes
 - [#359](https://github.com/influxdata/influxdb-client-go/pull/359) `WriteAPIBlocking.Flush()` correctly returns nil error. 
 
diff --git a/api/http/options.go b/api/http/options.go
index 66ef539..0f27505 100644
--- a/api/http/options.go
+++ b/api/http/options.go
@@ -23,6 +23,8 @@ type Options struct {
 	tlsConfig *tls.Config
 	// HTTP request timeout in sec. Default 20
 	httpRequestTimeout uint
+	// Application name in the User-Agent HTTP header string
+	appName string
 }
 
 // HTTPClient returns the http.Client that is configured to be used
@@ -119,6 +121,17 @@ func (o *Options) SetHTTPRequestTimeout(httpRequestTimeout uint) *Options {
 	return o
 }
 
+// ApplicationName returns application name used in the User-Agent HTTP header
+func (o *Options) ApplicationName() string {
+	return o.appName
+}
+
+// SetApplicationName sets an application name to the User-Agent HTTP header
+func (o *Options) SetApplicationName(appName string) *Options {
+	o.appName = appName
+	return o
+}
+
 // DefaultOptions returns Options object with default values
 func DefaultOptions() *Options {
 	return &Options{httpRequestTimeout: 20}
diff --git a/api/http/service.go b/api/http/service.go
index aa8cd1b..6316805 100644
--- a/api/http/service.go
+++ b/api/http/service.go
@@ -56,6 +56,7 @@ type service struct {
 	serverURL     string
 	authorization string
 	client        Doer
+	userAgent     string
 }
 
 // NewService creates instance of http Service with given parameters
@@ -73,6 +74,7 @@ func NewService(serverURL, authorization string, httpOptions *Options) Service {
 		serverURL:     serverURL,
 		authorization: authorization,
 		client:        httpOptions.HTTPDoer(),
+		userAgent:     http2.FormatUserAgent(httpOptions.ApplicationName()),
 	}
 }
 
@@ -128,7 +130,7 @@ func (s *service) DoHTTPRequestWithResponse(req *http.Request, requestCallback R
 		req.Header.Set("Authorization", s.authorization)
 	}
 	if req.Header.Get("User-Agent") == "" {
-		req.Header.Set("User-Agent", http2.UserAgent)
+		req.Header.Set("User-Agent", s.userAgent)
 	}
 	if requestCallback != nil {
 		requestCallback(req)
diff --git a/api/writeAPIBlocking.go b/api/writeAPIBlocking.go
index 4a6dcc9..eb3a768 100644
--- a/api/writeAPIBlocking.go
+++ b/api/writeAPIBlocking.go
@@ -86,9 +86,8 @@ func (w *writeAPIBlocking) write(ctx context.Context, line string) error {
 		w.batch = append(w.batch, line)
 		if len(w.batch) == int(w.writeOptions.BatchSize()) {
 			return w.flush(ctx)
-		} else {
-			return nil
 		}
+		return nil
 	}
 	err := w.service.WriteBatch(ctx, iwrite.NewBatch(line, w.writeOptions.MaxRetryTime()))
 	if err != nil {
diff --git a/client.go b/client.go
index c41fd1a..74e13cd 100644
--- a/client.go
+++ b/client.go
@@ -145,6 +145,9 @@ func NewClientWithOptions(serverURL string, authToken string, options *Options)
 		}
 		ilog.Infof("Using URL '%s'%s", serverURL, tokenStr)
 	}
+	if options.ApplicationName() == "" {
+		ilog.Warn("Application name is not set")
+	}
 	return client
 }
 
diff --git a/internal/http/userAgent.go b/internal/http/userAgent.go
index 0281bcb..cd27a9f 100644
--- a/internal/http/userAgent.go
+++ b/internal/http/userAgent.go
@@ -5,5 +5,17 @@
 // Package http hold internal HTTP related stuff
 package http
 
-// UserAgent keeps once created User-Agent string
-var UserAgent string
+import (
+	"fmt"
+)
+
+// UserAgentBase keeps once created base User-Agent string
+var UserAgentBase string
+
+// FormatUserAgent creates User-Agent header value for application name
+func FormatUserAgent(appName string) string {
+	if appName != "" {
+		return fmt.Sprintf("%s %s", UserAgentBase, appName)
+	}
+	return UserAgentBase
+}
diff --git a/options.go b/options.go
index 000bfc2..a2c903c 100644
--- a/options.go
+++ b/options.go
@@ -219,6 +219,17 @@ func (o *Options) AddDefaultTag(key, value string) *Options {
 	return o
 }
 
+// ApplicationName returns application name used in the User-Agent HTTP header
+func (o *Options) ApplicationName() string {
+	return o.HTTPOptions().ApplicationName()
+}
+
+// SetApplicationName sets an application name to the User-Agent HTTP header
+func (o *Options) SetApplicationName(appName string) *Options {
+	o.HTTPOptions().SetApplicationName(appName)
+	return o
+}
+
 // DefaultOptions returns Options object with default values
 func DefaultOptions() *Options {
 	return &Options{logLevel: 0, writeOptions: write.DefaultOptions(), httpOptions: http.DefaultOptions()}
diff --git a/version.go b/version.go
index 11f9d90..1e84253 100644
--- a/version.go
+++ b/version.go
@@ -17,5 +17,5 @@ const (
 )
 
 func init() {
-	http.UserAgent = fmt.Sprintf("influxdb-client-go/%s  (%s; %s)", Version, runtime.GOOS, runtime.GOARCH)
+	http.UserAgentBase = fmt.Sprintf("influxdb-client-go/%s (%s; %s)", Version, runtime.GOOS, runtime.GOARCH)
 }


Test patch:
diff --git a/api/http/options_test.go b/api/http/options_test.go
index 09cb9a9..e1a35f3 100644
--- a/api/http/options_test.go
+++ b/api/http/options_test.go
@@ -20,6 +20,7 @@ func TestDefaultOptions(t *testing.T) {
 	assert.Equal(t, uint(20), opts.HTTPRequestTimeout())
 	assert.NotNil(t, opts.HTTPClient())
 	assert.True(t, opts.OwnHTTPClient())
+	assert.EqualValues(t, "", opts.ApplicationName())
 }
 
 func TestOptionsSetting(t *testing.T) {
@@ -28,9 +29,11 @@ func TestOptionsSetting(t *testing.T) {
 	}
 	opts := http.DefaultOptions().
 		SetTLSConfig(tlsConfig).
-		SetHTTPRequestTimeout(50)
+		SetHTTPRequestTimeout(50).
+		SetApplicationName("Monitor/1.1")
 	assert.Equal(t, tlsConfig, opts.TLSConfig())
 	assert.Equal(t, uint(50), opts.HTTPRequestTimeout())
+	assert.EqualValues(t, "Monitor/1.1", opts.ApplicationName())
 	if client := opts.HTTPClient(); assert.NotNil(t, client) {
 		assert.Equal(t, 50*time.Second, client.Timeout)
 		assert.Equal(t, tlsConfig, client.Transport.(*nethttp.Transport).TLSClientConfig)
diff --git a/client_test.go b/client_test.go
index 31cb611..a6ed886 100644
--- a/client_test.go
+++ b/client_test.go
@@ -7,11 +7,16 @@ package influxdb2
 import (
 	"context"
 	"fmt"
+	ilog "github.com/influxdata/influxdb-client-go/v2/log"
+	"log"
 	"net/http"
 	"net/http/httptest"
+	"runtime"
+	"strings"
 	"testing"
 	"time"
 
+	ihttp "github.com/influxdata/influxdb-client-go/v2/api/http"
 	"github.com/influxdata/influxdb-client-go/v2/domain"
 	http2 "github.com/influxdata/influxdb-client-go/v2/internal/http"
 	iwrite "github.com/influxdata/influxdb-client-go/v2/internal/write"
@@ -76,10 +81,27 @@ func TestWriteAPIManagement(t *testing.T) {
 	assert.Len(t, c.syncWriteAPIs, 0)
 }
 
+func TestUserAgentBase(t *testing.T) {
+	ua := fmt.Sprintf("influxdb-client-go/%s (%s; %s)", Version, runtime.GOOS, runtime.GOARCH)
+	assert.Equal(t, ua, http2.UserAgentBase)
+
+}
+
+type doer struct {
+	userAgent string
+	doer      ihttp.Doer
+}
+
+func (d *doer) Do(req *http.Request) (*http.Response, error) {
+	req.Header.Set("User-Agent", d.userAgent)
+	return d.doer.Do(req)
+}
+
 func TestUserAgent(t *testing.T) {
+	ua := http2.UserAgentBase
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		<-time.After(100 * time.Millisecond)
-		if r.Header.Get("User-Agent") == http2.UserAgent {
+		if r.Header.Get("User-Agent") == ua {
 			w.WriteHeader(http.StatusNoContent)
 		} else {
 			w.WriteHeader(http.StatusNotFound)
@@ -87,13 +109,48 @@ func TestUserAgent(t *testing.T) {
 	}))
 
 	defer server.Close()
-	c := NewClient(server.URL, "x")
+	var sb strings.Builder
+	log.SetOutput(&sb)
+	log.SetFlags(0)
+	c := NewClientWithOptions(server.URL, "x", DefaultOptions().SetLogLevel(ilog.WarningLevel))
+	assert.True(t, strings.Contains(sb.String(), "Application name is not set"))
 	up, err := c.Ping(context.Background())
 	require.NoError(t, err)
 	assert.True(t, up)
 
 	err = c.WriteAPIBlocking("o", "b").WriteRecord(context.Background(), "a,a=a a=1i")
 	assert.NoError(t, err)
+
+	c.Close()
+	sb.Reset()
+	// Test setting application  name
+	c = NewClientWithOptions(server.URL, "x", DefaultOptions().SetApplicationName("Monitor/1.1"))
+	ua = fmt.Sprintf("influxdb-client-go/%s (%s; %s) Monitor/1.1", Version, runtime.GOOS, runtime.GOARCH)
+	assert.False(t, strings.Contains(sb.String(), "Application name is not set"))
+	up, err = c.Ping(context.Background())
+	require.NoError(t, err)
+	assert.True(t, up)
+
+	err = c.WriteAPIBlocking("o", "b").WriteRecord(context.Background(), "a,a=a a=1i")
+	assert.NoError(t, err)
+	c.Close()
+
+	ua = "Monitor/1.1"
+	opts := DefaultOptions()
+	opts.HTTPOptions().SetHTTPDoer(&doer{
+		userAgent: ua,
+		doer:      http.DefaultClient,
+	})
+
+	//Create client with custom user agent setter
+	c = NewClientWithOptions(server.URL, "x", opts)
+	up, err = c.Ping(context.Background())
+	require.NoError(t, err)
+	assert.True(t, up)
+
+	err = c.WriteAPIBlocking("o", "b").WriteRecord(context.Background(), "a,a=a a=1i")
+	assert.NoError(t, err)
+	c.Close()
 }
 
 func TestServerError429(t *testing.T) {
diff --git a/example_ua_set_test.go b/example_ua_set_test.go
new file mode 100644
index 0000000..b33e583
--- /dev/null
+++ b/example_ua_set_test.go
@@ -0,0 +1,54 @@
+// Copyright 2022 InfluxData, Inc. All rights reserved.
+// Use of this source code is governed by MIT
+// license that can be found in the LICENSE file.
+
+package influxdb2_test
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/influxdata/influxdb-client-go/v2"
+	ihttp "github.com/influxdata/influxdb-client-go/v2/api/http"
+)
+
+// UserAgentSetter is the implementation of Doer interface for setting User-Agent header
+type UserAgentSetter struct {
+	UserAgent   string
+	RequestDoer ihttp.Doer
+}
+
+// Do fulfills the Doer interface
+func (u *UserAgentSetter) Do(req *http.Request) (*http.Response, error) {
+	// Set User-Agent header to request
+	req.Header.Set("User-Agent", u.UserAgent)
+	// Call original Doer to proceed with request
+	return u.RequestDoer.Do(req)
+}
+
+func ExampleClient_customUserAgentHeader() {
+	// Set custom Doer to HTTPOptions
+	opts := influxdb2.DefaultOptions()
+	opts.HTTPOptions().SetHTTPDoer(&UserAgentSetter{
+		UserAgent:   "NetMonitor/1.1",
+		RequestDoer: http.DefaultClient,
+	})
+
+	//Create client with customized options
+	client := influxdb2.NewClientWithOptions("http://localhost:8086", "my-token", opts)
+
+	// Always close client at the end
+	defer client.Close()
+
+	// Issue a call with custom User-Agent header
+	resp, err := client.Ping(context.Background())
+	if err != nil {
+		panic(err)
+	}
+	if resp {
+		fmt.Println("Server is up")
+	} else {
+		fmt.Println("Server is down")
+	}
+}
diff --git a/options_test.go b/options_test.go
index 6afe193..5c9dcdb 100644
--- a/options_test.go
+++ b/options_test.go
@@ -33,6 +33,7 @@ func TestDefaultOptions(t *testing.T) {
 	assert.EqualValues(t, (*tls.Config)(nil), opts.TLSConfig())
 	assert.EqualValues(t, 20, opts.HTTPRequestTimeout())
 	assert.EqualValues(t, 0, opts.LogLevel())
+	assert.EqualValues(t, "", opts.ApplicationName())
 }
 
 func TestSettingsOptions(t *testing.T) {
@@ -53,7 +54,8 @@ func TestSettingsOptions(t *testing.T) {
 		SetTLSConfig(tlsConfig).
 		SetHTTPRequestTimeout(50).
 		SetLogLevel(3).
-		AddDefaultTag("t", "a")
+		AddDefaultTag("t", "a").
+		SetApplicationName("Monitor/1.1")
 	assert.EqualValues(t, 5, opts.BatchSize())
 	assert.EqualValues(t, true, opts.UseGZip())
 	assert.EqualValues(t, 5_000, opts.FlushInterval())
@@ -66,6 +68,7 @@ func TestSettingsOptions(t *testing.T) {
 	assert.EqualValues(t, 5, opts.ExponentialBase())
 	assert.EqualValues(t, tlsConfig, opts.TLSConfig())
 	assert.EqualValues(t, 50, opts.HTTPRequestTimeout())
+	assert.EqualValues(t, "Monitor/1.1", opts.ApplicationName())
 	if client := opts.HTTPClient(); assert.NotNil(t, client) {
 		assert.EqualValues(t, 50*time.Second, client.Timeout)
 		assert.Equal(t, tlsConfig, client.Transport.(*http.Transport).TLSClientConfig)