Use system info and build info as user agent, and expose DebugInfo (#41)

* Refactor setting and unsetting system info

* detect version on init

* Update client.go

* Update client_test.go

Co-authored-by: Copilot <[email protected]>

* Update client.go

Co-authored-by: Copilot <[email protected]>

---------

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Nikola Jokic
2025-12-17 16:29:45 +01:00
committed by GitHub
co-authored by Copilot
parent 40342b1e2a
commit f3ab19d2b8
5 changed files with 467 additions and 131 deletions
+116 -45
View File
@@ -60,6 +60,17 @@ type Client struct {
config *gitHubConfig
logger *slog.Logger
buildInfo clientBuildInfo
// systemInfoMu guards setting system info.
systemInfoMu sync.Mutex
systemInfo SystemInfo
// userAgent is computed based on buildInfo and systemInfo.
// userAgent should be re-computed every time client.SetSystemInfo
// is called.
//
// On every call, load the userAgent first locally so we can
// avoid lock-unlock on every call.
userAgent atomic.Pointer[string]
rootCAs *x509.CertPool
@@ -68,6 +79,31 @@ type Client struct {
proxyFunc ProxyFunc
}
type clientBuildInfo struct {
version string
commitSHA string
}
type debugInfo struct {
HasProxy bool `json:"has_proxy"`
HasRootCA bool `json:"has_root_ca"`
SystemInfo string `json:"system_info"`
}
// DebugInfo returns a JSON string containing debug information about the client,
// including whether a proxy or custom root CA is configured, and the current system info.
// This method is intended for diagnostic and troubleshooting purposes.
func (c *Client) DebugInfo() string {
info := debugInfo{
HasProxy: c.proxyFunc != nil,
HasRootCA: c.rootCAs != nil,
SystemInfo: *c.userAgent.Load(),
}
b, _ := json.Marshal(info)
return string(b)
}
// GitHubAppAuth contains the GitHub App authentication credentials. All fields are required.
type GitHubAppAuth struct {
// ClientID is the Client ID of the application (app id also works)
@@ -105,37 +141,27 @@ type ProxyFunc func(req *http.Request) (*url.URL, error)
// Option defines a functional option for configuring the Client.
type Option func(*Client)
// UserAgentInfo contains information for constructing the user agent string.
type UserAgentInfo struct {
// SystemInfo contains information about the system that uses the
// scaleset client.
//
// For example, when Actions Runner Controller uses the scaleset API,
// it will set the following:
// - System: "actions-runner-controller"
// - Version: "release-version"
// - CommitSHA: "sha-of-the-release-commit"
// - Subsystem: "listener" or "controller"
type SystemInfo struct {
// System is the name of the scale set implementation
System string
System string `json:"system"`
// Version is the version of the controller
Version string
Version string `json:"version"`
// CommitSHA is the git commit SHA of the controller
CommitSHA string
CommitSHA string `json:"commit_sha"`
// ScaleSetID is the ID of the scale set
ScaleSetID int
ScaleSetID int `json:"scale_set_id"`
// Subsystem is the subsystem such as listener, controller, etc.
// Each system may pick its own subsystem name.
Subsystem string
}
func (u UserAgentInfo) String() string {
scaleSetID := "NA"
if u.ScaleSetID > 0 {
scaleSetID = strconv.Itoa(u.ScaleSetID)
}
return fmt.Sprintf(
"%s/%s (%s; %s) ScaleSetID/%s; client (%s; %s)",
u.System,
u.Version,
u.CommitSHA,
u.Subsystem,
scaleSetID,
packageVersion,
commitSHA,
)
Subsystem string `json:"subsystem"`
}
// WithLogger sets a custom logger for the Client.
@@ -180,23 +206,45 @@ func WithProxy(proxyFunc ProxyFunc) Option {
}
}
type ClientWithGitHubAppConfig struct {
GitHubConfigURL string
GitHubAppAuth GitHubAppAuth
SystemInfo SystemInfo
}
// NewClientWithGitHubApp creates a new Client using GitHub App credentials.
func NewClientWithGitHubApp(githubConfigURL string, appCreds *GitHubAppAuth, options ...Option) (*Client, error) {
func NewClientWithGitHubApp(config ClientWithGitHubAppConfig, options ...Option) (*Client, error) {
creds := &actionsAuth{
app: appCreds,
app: &config.GitHubAppAuth,
}
return newClient(githubConfigURL, creds, options...)
return newClient(
config.SystemInfo,
config.GitHubConfigURL,
creds,
options...,
)
}
type NewClientWithPersonalAccessTokenConfig struct {
GitHubConfigURL string
PersonalAccessToken string
SystemInfo SystemInfo
}
// NewClientWithPersonalAccessToken creates a new Client using a personal access token.
func NewClientWithPersonalAccessToken(githubConfigURL, personalAccessToken string, options ...Option) (*Client, error) {
func NewClientWithPersonalAccessToken(config NewClientWithPersonalAccessTokenConfig, options ...Option) (*Client, error) {
creds := &actionsAuth{
token: personalAccessToken,
token: config.PersonalAccessToken,
}
return newClient(githubConfigURL, creds, options...)
return newClient(
config.SystemInfo,
config.GitHubConfigURL,
creds,
options...,
)
}
func newClient(githubConfigURL string, creds *actionsAuth, options ...Option) (*Client, error) {
func newClient(systemInfo SystemInfo, githubConfigURL string, creds *actionsAuth, options ...Option) (*Client, error) {
config, err := parseGitHubConfigFromURL(githubConfigURL)
if err != nil {
return nil, fmt.Errorf("failed to parse githubConfigURL: %w", err)
@@ -210,17 +258,14 @@ func newClient(githubConfigURL string, creds *actionsAuth, options ...Option) (*
// retryablehttp defaults
retryMax: 4,
retryWaitMax: 30 * time.Second,
buildInfo: clientBuildInfo{
version: packageVersion,
commitSHA: commitSHA,
},
}
version, sha := detectModuleVersionAndCommit()
userAgent := UserAgentInfo{
System: "scaleset-client",
Version: version,
CommitSHA: sha,
Subsystem: "NA",
ScaleSetID: 0,
}.String()
ac.userAgent.Store(&userAgent)
ac.SetSystemInfo(systemInfo)
for _, option := range options {
option(ac)
@@ -267,10 +312,36 @@ func (c *Client) newRetryableHTTPClient() (*retryablehttp.Client, error) {
return retryClient, nil
}
// SetUserAgent updates the user agent
func (c *Client) SetUserAgent(info UserAgentInfo) {
v := info.String()
c.userAgent.Store(&v)
// SetSystemInfo updates the information about the system.
func (c *Client) SetSystemInfo(info SystemInfo) {
c.systemInfoMu.Lock()
defer c.systemInfoMu.Unlock()
c.systemInfo = info
c.setUserAgent()
}
// SystemInfo returns the current system info that the client
// has configured.
func (c *Client) SystemInfo() SystemInfo {
c.systemInfoMu.Lock()
defer c.systemInfoMu.Unlock()
return c.systemInfo
}
type userAgent struct {
SystemInfo
BuildVersion string `json:"build_version"`
BuildCommitSHA string `json:"build_commit_sha"`
}
func (c *Client) setUserAgent() {
b, _ := json.Marshal(userAgent{
SystemInfo: c.systemInfo,
BuildVersion: c.buildInfo.version,
BuildCommitSHA: c.buildInfo.commitSHA,
})
userAgent := string(b)
c.userAgent.Store(&userAgent)
}
func (c *Client) do(req *http.Request) (*http.Response, error) {