diff --git a/changelog/fragments/1730288427-shared-host.yaml b/changelog/fragments/1730288427-shared-host.yaml new file mode 100644 index 00000000000..5eb437ba0b5 --- /dev/null +++ b/changelog/fragments/1730288427-shared-host.yaml @@ -0,0 +1,32 @@ +# Kind can be one of: +# - breaking-change: a change to previously-documented behavior +# - deprecation: functionality that is being removed in a later release +# - bug-fix: fixes a problem in a previous version +# - enhancement: extends functionality but does not break or fix existing behavior +# - feature: new functionality +# - known-issue: problems that we are aware of in a given version +# - security: impacts on the security of a product or a user’s deployment. +# - upgrade: important information for someone upgrading from a prior version +# - other: does not fit into any of the other categories +kind: enhancement + +# Change summary; a 80ish characters long description of the change. +summary: Collect host info exactly once on startup + +# Long description; in case the summary is not enough to describe the change +# this field accommodate a description without length limits. +# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment. +#description: + +# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc. +component: elastic-agent + +# PR URL; optional; the PR number that added the changeset. +# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added. +# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number. +# Please provide it if you are adding a fragment for a different PR. +#pr: https://github.com/owner/repo/1234 + +# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of). +# If not present is automatically filled by the tooling with the issue linked to the PR number. +#issue: https://github.com/owner/repo/1234 diff --git a/internal/pkg/agent/application/info/agent_metadata.go b/internal/pkg/agent/application/info/agent_metadata.go index 012b7911063..0924c1a1df7 100644 --- a/internal/pkg/agent/application/info/agent_metadata.go +++ b/internal/pkg/agent/application/info/agent_metadata.go @@ -18,7 +18,6 @@ import ( "github.com/elastic/elastic-agent/internal/pkg/util" "github.com/elastic/elastic-agent/pkg/core/logger" - "github.com/elastic/go-sysinfo" "github.com/elastic/go-sysinfo/types" ) @@ -146,7 +145,7 @@ func Metadata(ctx context.Context, l *logger.Logger) (*ECSMeta, error) { // ECSMetadata returns an agent ECS compliant metadata. func (i *AgentInfo) ECSMetadata(l *logger.Logger) (*ECSMeta, error) { - sysInfo, err := sysinfo.Host() + sysInfo, err := util.GetHost() if err != nil { return nil, err } @@ -195,7 +194,7 @@ func (i *AgentInfo) ECSMetadataFlatMap(l *logger.Logger) (map[string]interface{} // TODO: remove these values when kibana migrates to ECS meta := make(map[string]interface{}) - sysInfo, err := sysinfo.Host() + sysInfo, err := util.GetHost() if err != nil { return nil, err } diff --git a/internal/pkg/agent/application/info/agent_metadata_test.go b/internal/pkg/agent/application/info/agent_metadata_test.go index 5a951cf63ed..d1e248c3118 100644 --- a/internal/pkg/agent/application/info/agent_metadata_test.go +++ b/internal/pkg/agent/application/info/agent_metadata_test.go @@ -11,8 +11,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/elastic/go-sysinfo" - "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" "github.com/elastic/elastic-agent/internal/pkg/release" "github.com/elastic/elastic-agent/internal/pkg/testutils" @@ -34,7 +32,7 @@ func TestECSMetadata(t *testing.T) { assert.NotNil(t, metadata.Elastic.Agent, "metadata.Elastic.Agent must not be nil") } - sysInfo, err := sysinfo.Host() + sysInfo, err := util.GetHost() require.NoError(t, err) info := sysInfo.Info() diff --git a/internal/pkg/agent/application/info/inject_config.go b/internal/pkg/agent/application/info/inject_config.go index 025471bafc8..0d3aac9bccf 100644 --- a/internal/pkg/agent/application/info/inject_config.go +++ b/internal/pkg/agent/application/info/inject_config.go @@ -7,10 +7,11 @@ package info import ( "runtime" + "github.com/elastic/elastic-agent/internal/pkg/util" + "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" "github.com/elastic/elastic-agent/internal/pkg/agent/errors" "github.com/elastic/elastic-agent/internal/pkg/config" - "github.com/elastic/go-sysinfo" ) // InjectAgentConfig injects config to a provided configuration. @@ -30,7 +31,7 @@ func InjectAgentConfig(c *config.Config) error { // agentGlobalConfig gets global config used for resolution of variables inside configuration // such as ${path.data}. func agentGlobalConfig() (map[string]interface{}, error) { - hostInfo, err := sysinfo.Host() + hostInfo, err := util.GetHost() if err != nil { return nil, err } diff --git a/internal/pkg/composable/providers/host/host.go b/internal/pkg/composable/providers/host/host.go index 5255ccb7bc6..165469af0c4 100644 --- a/internal/pkg/composable/providers/host/host.go +++ b/internal/pkg/composable/providers/host/host.go @@ -132,6 +132,7 @@ func ContextProviderBuilder(log *logger.Logger, c *config.Config, _ bool) (corec func getHostInfo(log *logger.Logger) func() (map[string]interface{}, error) { return func() (map[string]interface{}, error) { + // We don't use the shared host info from util here, as we explicitly want the latest host information on every call. sysInfo, err := sysinfo.Host() if err != nil { return nil, err diff --git a/internal/pkg/util/host.go b/internal/pkg/util/host.go index a4fd8182ca5..f36df0f66dd 100644 --- a/internal/pkg/util/host.go +++ b/internal/pkg/util/host.go @@ -6,9 +6,11 @@ package util import ( "context" + "sync" "time" "github.com/elastic/elastic-agent/pkg/core/logger" + "github.com/elastic/go-sysinfo" "github.com/elastic/go-sysinfo/types" ) @@ -31,3 +33,61 @@ func GetHostName(isFqdnFeatureEnabled bool, hostInfo types.HostInfo, host types. return fqdn } + +var _ types.Host = &threadSafeHost{} + +// threadSafeHost is a thread-safe wrapper around types.Host. +// It exists so we can only create it once, as some of the setup it does is relatively expensive. +type threadSafeHost struct { + sync.Mutex + inner types.Host +} + +func newThreadSafeHost(inner types.Host) *threadSafeHost { + return &threadSafeHost{inner: inner} +} + +func (s *threadSafeHost) CPUTime() (types.CPUTimes, error) { + s.Lock() + defer s.Unlock() + return s.inner.CPUTime() +} + +func (s *threadSafeHost) Info() types.HostInfo { + s.Lock() + defer s.Unlock() + return s.inner.Info() +} + +func (s *threadSafeHost) Memory() (*types.HostMemoryInfo, error) { + s.Lock() + defer s.Unlock() + return s.inner.Memory() +} + +func (s *threadSafeHost) FQDNWithContext(ctx context.Context) (string, error) { + s.Lock() + defer s.Unlock() + return s.inner.FQDNWithContext(ctx) +} + +func (s *threadSafeHost) FQDN() (string, error) { + s.Lock() + defer s.Unlock() + return s.inner.FQDN() +} + +var ( + sharedHost types.Host + once sync.Once + hostErr error +) + +func GetHost() (types.Host, error) { + once.Do(func() { + var innerHost types.Host + innerHost, hostErr = sysinfo.Host() + sharedHost = newThreadSafeHost(innerHost) + }) + return sharedHost, hostErr +} diff --git a/internal/pkg/util/host_test.go b/internal/pkg/util/host_test.go index ebd8b798678..2d98a9756a6 100644 --- a/internal/pkg/util/host_test.go +++ b/internal/pkg/util/host_test.go @@ -11,6 +11,7 @@ import ( "github.com/elastic/elastic-agent-libs/logp" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/elastic/go-sysinfo/types" @@ -59,6 +60,25 @@ func TestGetHostName(t *testing.T) { } } +func TestSharedHost(t *testing.T) { + innerHost := &mockHost{} + shared := newThreadSafeHost(innerHost) + + innerCpuTime, _ := innerHost.CPUTime() + sharedCpuTime, _ := shared.CPUTime() + assert.Equal(t, innerCpuTime, sharedCpuTime) + + assert.Equal(t, innerHost.Info(), shared.Info()) + + innerMemoryInfo, _ := innerHost.Memory() + sharedMemoryInfo, _ := shared.Memory() + assert.Equal(t, innerMemoryInfo, sharedMemoryInfo) + + innerFQDN, _ := innerHost.FQDN() + sharedFQDN, _ := shared.FQDN() + assert.Equal(t, innerFQDN, sharedFQDN) +} + type mockHost struct { fqdn string fqdnErr error diff --git a/pkg/component/platforms.go b/pkg/component/platforms.go index 0875cccc22a..97a536456a7 100644 --- a/pkg/component/platforms.go +++ b/pkg/component/platforms.go @@ -9,8 +9,7 @@ import ( goruntime "runtime" "strings" - "github.com/elastic/go-sysinfo" - + "github.com/elastic/elastic-agent/internal/pkg/util" "github.com/elastic/elastic-agent/pkg/utils" ) @@ -126,7 +125,7 @@ func LoadPlatformDetail(modifiers ...PlatformModifier) (PlatformDetail, error) { if err != nil { return PlatformDetail{}, err } - info, err := sysinfo.Host() + info, err := util.GetHost() if err != nil { return PlatformDetail{}, err } diff --git a/pkg/testing/define/define.go b/pkg/testing/define/define.go index 3d83b60267d..5d5ee618d19 100644 --- a/pkg/testing/define/define.go +++ b/pkg/testing/define/define.go @@ -21,9 +21,9 @@ import ( "github.com/elastic/elastic-agent-libs/kibana" "github.com/elastic/go-elasticsearch/v8" - "github.com/elastic/go-sysinfo" "github.com/elastic/go-sysinfo/types" + "github.com/elastic/elastic-agent/internal/pkg/util" atesting "github.com/elastic/elastic-agent/pkg/testing" "github.com/elastic/elastic-agent/pkg/utils" semver "github.com/elastic/elastic-agent/pkg/version" @@ -205,7 +205,7 @@ func runOrSkip(t *testing.T, req Requirements, local bool, kubernetes bool) *Inf func getOSInfo() (*types.OSInfo, error) { osInfoOnce.Do(func() { - sysInfo, err := sysinfo.Host() + sysInfo, err := util.GetHost() if err != nil { osInfoErr = err } else {