diff --git a/NOTICE.txt b/NOTICE.txt index 2c157184941..00dd3a820e7 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -21730,11 +21730,11 @@ THE SOFTWARE. -------------------------------------------------------------------------------- Dependency : github.com/gobuffalo/here -Version: v0.6.0 +Version: v0.6.7 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/gobuffalo/here@v0.6.0/LICENSE: +Contents of probable licence file $GOMODCACHE/github.com/gobuffalo/here@v0.6.7/LICENSE: The MIT License (MIT) @@ -29554,11 +29554,11 @@ SOFTWARE. -------------------------------------------------------------------------------- Dependency : github.com/karrick/godirwalk -Version: v1.16.1 +Version: v1.17.0 Licence type (autodetected): BSD-2-Clause -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/karrick/godirwalk@v1.16.1/LICENSE: +Contents of probable licence file $GOMODCACHE/github.com/karrick/godirwalk@v1.17.0/LICENSE: BSD 2-Clause License @@ -30478,11 +30478,11 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI -------------------------------------------------------------------------------- Dependency : github.com/markbates/pkger -Version: v0.17.0 +Version: v0.17.1 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/markbates/pkger@v0.17.0/LICENSE: +Contents of probable licence file $GOMODCACHE/github.com/markbates/pkger@v0.17.1/LICENSE: The MIT License (MIT) @@ -38665,11 +38665,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- Dependency : github.com/sergi/go-diff -Version: v1.2.0 +Version: v1.3.1 Licence type (autodetected): MIT -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/sergi/go-diff@v1.2.0/LICENSE: +Contents of probable licence file $GOMODCACHE/github.com/sergi/go-diff@v1.3.1/LICENSE: Copyright (c) 2012-2016 The go-diff Authors. All rights reserved. @@ -39511,11 +39511,11 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice -------------------------------------------------------------------------------- Dependency : github.com/spf13/afero -Version: v1.9.5 +Version: v1.10.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/spf13/afero@v1.9.5/LICENSE.txt: +Contents of probable licence file $GOMODCACHE/github.com/spf13/afero@v1.10.0/LICENSE.txt: Apache License Version 2.0, January 2004 diff --git a/changelog/fragments/1719345278-container.yaml b/changelog/fragments/1719345278-container.yaml new file mode 100644 index 00000000000..944ae6c8823 --- /dev/null +++ b/changelog/fragments/1719345278-container.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: feature + +# Change summary; a 80ish characters long description of the change. +summary: Event logger output now can be set via Fleet overrides or environment variable for container command. + +# 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/elastic/elastic-agent/pull/4932 + +# 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: diff --git a/go.mod b/go.mod index 520cb124009..8b8435f44bf 100644 --- a/go.mod +++ b/go.mod @@ -164,7 +164,7 @@ require ( github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/go-viper/mapstructure/v2 v2.0.0 // indirect - github.com/gobuffalo/here v0.6.0 // indirect + github.com/gobuffalo/here v0.6.7 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -192,7 +192,7 @@ require ( github.com/jcchavezs/porto v0.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/karrick/godirwalk v1.16.1 // indirect + github.com/karrick/godirwalk v1.17.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect github.com/knadh/koanf/providers/confmap v0.1.0 // indirect @@ -203,7 +203,7 @@ require ( github.com/lightstep/go-expohisto v1.0.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/markbates/pkger v0.17.0 // indirect + github.com/markbates/pkger v0.17.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect @@ -246,9 +246,10 @@ require ( github.com/rivo/uniseg v0.4.4 // indirect github.com/rs/cors v1.11.0 // indirect github.com/santhosh-tekuri/jsonschema v1.2.4 // indirect - github.com/sergi/go-diff v1.2.0 // indirect + github.com/sergi/go-diff v1.3.1 // indirect github.com/shirou/gopsutil/v4 v4.24.5 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/spf13/afero v1.10.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tilinna/clock v1.1.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect diff --git a/go.sum b/go.sum index 18d1fd0d161..98b37b927b6 100644 --- a/go.sum +++ b/go.sum @@ -996,8 +996,9 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI= github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= +github.com/gobuffalo/here v0.6.7 h1:hpfhh+kt2y9JLDfhYUxxCRxQol540jsVfKUZzjlbp8o= +github.com/gobuffalo/here v0.6.7/go.mod h1:vuCfanjqckTuRlqAitJz6QC4ABNnS27wLb816UhsPcc= github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= @@ -1338,8 +1339,9 @@ github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1 github.com/kardianos/service v1.2.1-0.20210728001519-a323c3813bc7 h1:oohm9Rk9JAxxmp2NLZa7Kebgz9h4+AJDcc64txg3dQ0= github.com/kardianos/service v1.2.1-0.20210728001519-a323c3813bc7/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/karrick/godirwalk v1.15.6/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= -github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI= +github.com/karrick/godirwalk v1.17.0/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= @@ -1418,8 +1420,9 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= -github.com/markbates/pkger v0.17.0 h1:RFfyBPufP2V6cddUyyEVSHBpaAnM1WzaMNyqomeT+iY= github.com/markbates/pkger v0.17.0/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= +github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno= +github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= @@ -1846,8 +1849,9 @@ github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvW github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shirou/gopsutil/v3 v3.23.2/go.mod h1:gv0aQw33GLo3pG8SiWKiQrbDzbRY1K80RyZJ7V4Th1M= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= @@ -1882,8 +1886,9 @@ github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= diff --git a/internal/pkg/agent/application/actions/handlers/handler_action_policy_change.go b/internal/pkg/agent/application/actions/handlers/handler_action_policy_change.go index e7b94c18c20..ff529545946 100644 --- a/internal/pkg/agent/application/actions/handlers/handler_action_policy_change.go +++ b/internal/pkg/agent/application/actions/handlers/handler_action_policy_change.go @@ -46,6 +46,7 @@ type PolicyChangeHandler struct { ch chan coordinator.ConfigChange setters []actions.ClientSetter policyLogLevelSetter logLevelSetter + coordinator *coordinator.Coordinator // Disabled for 8.8.0 release in order to limit the surface // https://github.com/elastic/security-team/issues/6501 // // Last known valid signature validation key @@ -60,6 +61,7 @@ func NewPolicyChangeHandler( store storage.Store, ch chan coordinator.ConfigChange, policyLogLevelSetter logLevelSetter, + coordinator *coordinator.Coordinator, setters ...actions.ClientSetter, ) *PolicyChangeHandler { return &PolicyChangeHandler{ @@ -69,6 +71,7 @@ func NewPolicyChangeHandler( store: store, ch: ch, setters: setters, + coordinator: coordinator, policyLogLevelSetter: policyLogLevelSetter, } } @@ -305,6 +308,15 @@ func (h *PolicyChangeHandler) handlePolicyChange(ctx context.Context, c *config. h.config.Fleet.Client = *validatedConfig } + cfg, err := configuration.NewFromConfig(c) + if err != nil { + return errors.New(err, "could not parse the configuration from the policy", errors.TypeConfig) + } + hasEventLoggingOutputChanged := h.hasEventLoggingOutputChanged(cfg) + if hasEventLoggingOutputChanged { + h.config.Settings.EventLoggingConfig = cfg.Settings.EventLoggingConfig + } + // persist configuration err = saveConfig(h.agentInfo, h.config, h.store) if err != nil { @@ -317,9 +329,30 @@ func (h *PolicyChangeHandler) handlePolicyChange(ctx context.Context, c *config. return fmt.Errorf("applying FleetClientConfig: %w", err) } + // If the event logging output has changed, we need to + // re-exec the Elastic-Agent to apply the new logging + // output. + // The new logging configuration has already been persisted + // to the disk, the Elastic-Agent will pick it up once it starts. + if hasEventLoggingOutputChanged { + h.coordinator.ReExec(nil) + } + return nil } +// hasEventLoggingOutputChanged returns true if the output of the event logger has changed +func (p *PolicyChangeHandler) hasEventLoggingOutputChanged(new *configuration.Configuration) bool { + switch { + case p.config.Settings.EventLoggingConfig.ToFiles != new.Settings.EventLoggingConfig.ToFiles: + return true + case p.config.Settings.EventLoggingConfig.ToStderr != new.Settings.EventLoggingConfig.ToStderr: + return true + default: + return false + } +} + func validateLoggingConfig(cfg *config.Config) (*logger.Config, error) { parsedConfig, err := configuration.NewPartialFromConfigNoDefaults(cfg) @@ -445,12 +478,14 @@ func clientEqual(k1 remote.Config, k2 remote.Config) bool { func fleetToReader(agentID string, headers map[string]string, cfg *configuration.Configuration) (io.Reader, error) { configToStore := map[string]interface{}{ "fleet": cfg.Fleet, - "agent": map[string]interface{}{ - "id": agentID, - "headers": headers, - "logging.level": cfg.Settings.LoggingConfig.Level, - "monitoring.http": cfg.Settings.MonitoringConfig.HTTP, - "monitoring.pprof": cfg.Settings.MonitoringConfig.Pprof, + "agent": map[string]interface{}{ // Add event logging configuration here! + "id": agentID, + "headers": headers, + "logging.level": cfg.Settings.LoggingConfig.Level, + "logging.event_data.to_files": cfg.Settings.EventLoggingConfig.ToFiles, + "logging.event_data.to_stderr": cfg.Settings.EventLoggingConfig.ToStderr, + "monitoring.http": cfg.Settings.MonitoringConfig.HTTP, + "monitoring.pprof": cfg.Settings.MonitoringConfig.Pprof, }, } diff --git a/internal/pkg/agent/application/actions/handlers/handler_action_policy_change_test.go b/internal/pkg/agent/application/actions/handlers/handler_action_policy_change_test.go index 53aee3e101b..530865fc16c 100644 --- a/internal/pkg/agent/application/actions/handlers/handler_action_policy_change_test.go +++ b/internal/pkg/agent/application/actions/handlers/handler_action_policy_change_test.go @@ -62,8 +62,7 @@ func TestPolicyChange(t *testing.T) { } cfg := configuration.DefaultConfiguration() - - handler := NewPolicyChangeHandler(log, agentInfo, cfg, nullStore, ch, nilLogLevelSet(t)) + handler := NewPolicyChangeHandler(log, agentInfo, cfg, nullStore, ch, nilLogLevelSet(t), &coordinator.Coordinator{}) err := handler.Handle(context.Background(), action, ack) require.NoError(t, err) @@ -94,7 +93,7 @@ func TestPolicyAcked(t *testing.T) { } cfg := configuration.DefaultConfiguration() - handler := NewPolicyChangeHandler(log, agentInfo, cfg, nullStore, ch, nilLogLevelSet(t)) + handler := NewPolicyChangeHandler(log, agentInfo, cfg, nullStore, ch, nilLogLevelSet(t), &coordinator.Coordinator{}) err := handler.Handle(context.Background(), action, tacker) require.NoError(t, err) diff --git a/internal/pkg/agent/application/managed_mode.go b/internal/pkg/agent/application/managed_mode.go index 3d36ef01fa1..7167c31564d 100644 --- a/internal/pkg/agent/application/managed_mode.go +++ b/internal/pkg/agent/application/managed_mode.go @@ -343,6 +343,7 @@ func (m *managedConfigManager) initDispatcher(canceller context.CancelFunc) *han m.store, m.ch, settingsHandler, + m.coord, ) m.dispatcher.MustRegister( diff --git a/internal/pkg/agent/cmd/container.go b/internal/pkg/agent/cmd/container.go index 0e4b375314f..eec934dad56 100644 --- a/internal/pkg/agent/cmd/container.go +++ b/internal/pkg/agent/cmd/container.go @@ -25,6 +25,7 @@ import ( "gopkg.in/yaml.v2" "github.com/elastic/elastic-agent-libs/kibana" + "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent-libs/transport/httpcommon" "github.com/elastic/elastic-agent-libs/transport/tlscommon" "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" @@ -136,6 +137,11 @@ be used when the same credentials will be used across all the possible actions a ELASTIC_AGENT_TAGS - user provided tags for the agent [linux,staging] +* Elastic-Agent event logging + If EVENTS_TO_STDERR is set to true log entries containing event data or whole raw events will be logged to stderr alongside +all other log entries. If unset or set to false, the events will be logged to a separate file that is not collected alongside +the monitoring logs, however they will be present in diagnostics. + By default when this command starts it will check for an existing fleet.yml. If that file already exists then all the above actions will be skipped, because the Elastic Agent has already been enrolled. To ensure that enrollment occurs on every start of the container set FLEET_FORCE to 1. @@ -769,6 +775,16 @@ func logToStderr(cfg *configuration.Configuration) { cfg.Settings.LoggingConfig.ToStderr = true cfg.Settings.LoggingConfig.ToFiles = false } + + eventsToStderrEnv := envWithDefault("false", "EVENTS_TO_STDERR") + eventsToStderr, err := strconv.ParseBool(eventsToStderrEnv) + if err != nil { + logp.Warn("cannot parse EVENS_TO_STDERR='%s' as boolean, logging events to file'", eventsToStderrEnv) + } + if eventsToStderr { + cfg.Settings.EventLoggingConfig.ToFiles = false + cfg.Settings.EventLoggingConfig.ToStderr = true + } } func setPaths(statePath, configPath, logsPath, socketPath string, writePaths bool) error { diff --git a/testing/integration/container_cmd_test.go b/testing/integration/container_cmd_test.go index 35c9dab37f0..92e8dcd6eb9 100644 --- a/testing/integration/container_cmd_test.go +++ b/testing/integration/container_cmd_test.go @@ -7,16 +7,23 @@ package integration import ( + "bufio" + "bytes" "context" + "encoding/json" "fmt" + "net/http" + "net/http/httputil" "os" "os/exec" "path/filepath" "strings" "testing" + "text/template" "time" "github.com/google/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/elastic/elastic-agent-libs/kibana" @@ -26,9 +33,16 @@ import ( "github.com/elastic/elastic-agent/pkg/testing/tools/fleettools" ) -func createPolicy(t *testing.T, ctx context.Context, agentFixture *atesting.Fixture, info *define.Info) string { +func createPolicy( + t *testing.T, + ctx context.Context, + agentFixture *atesting.Fixture, + info *define.Info, + policyName string, + dataOutputID string) (string, string) { + createPolicyReq := kibana.AgentPolicy{ - Name: fmt.Sprintf("test-policy-enroll-%s", uuid.New().String()), + Name: policyName, Namespace: info.Namespace, Description: "test policy for agent enrollment", MonitoringEnabled: []kibana.MonitoringEnabledOption{ @@ -43,6 +57,10 @@ func createPolicy(t *testing.T, ctx context.Context, agentFixture *atesting.Fixt }, } + if dataOutputID != "" { + createPolicyReq.DataOutputID = dataOutputID + } + // Create policy policy, err := info.KibanaClient.CreatePolicy(ctx, createPolicyReq) if err != nil { @@ -60,11 +78,17 @@ func createPolicy(t *testing.T, ctx context.Context, agentFixture *atesting.Fixt t.Fatalf("unable to create enrolment API key: %s", err) } - return enrollmentToken.APIKey + return policy.ID, enrollmentToken.APIKey } -func prepareContainerCMD(t *testing.T, ctx context.Context, agentFixture *atesting.Fixture, info *define.Info, env []string) *exec.Cmd { - cmd, err := agentFixture.PrepareAgentCommand(ctx, []string{"container"}) +func prepareAgentCMD( + t *testing.T, + ctx context.Context, + agentFixture *atesting.Fixture, + args []string, + env []string) (*exec.Cmd, *strings.Builder) { + + cmd, err := agentFixture.PrepareAgentCommand(ctx, args) if err != nil { t.Fatalf("could not prepare agent command: %s", err) } @@ -96,7 +120,7 @@ func prepareContainerCMD(t *testing.T, ctx context.Context, agentFixture *atesti cmd.Stderr = &agentOutput cmd.Stdout = &agentOutput cmd.Env = append(os.Environ(), env...) - return cmd + return cmd, &agentOutput } func TestContainerCMD(t *testing.T) { @@ -126,7 +150,13 @@ func TestContainerCMD(t *testing.T) { t.Fatalf("could not get Fleet URL: %s", err) } - enrollmentToken := createPolicy(t, ctx, agentFixture, info) + _, enrollmentToken := createPolicy( + t, + ctx, + agentFixture, + info, + fmt.Sprintf("%s-%s", t.Name(), uuid.New().String()), + "") env := []string{ "FLEET_ENROLL=1", "FLEET_URL=" + fleetURL, @@ -138,14 +168,12 @@ func TestContainerCMD(t *testing.T) { "STATE_PATH=" + agentFixture.WorkDir(), } - cmd := prepareContainerCMD(t, ctx, agentFixture, info, env) + cmd, agentOutput := prepareAgentCMD(t, ctx, agentFixture, []string{"container"}, env) t.Logf(">> running binary with: %v", cmd.Args) if err := cmd.Start(); err != nil { t.Fatalf("error running container cmd: %s", err) } - agentOutput := cmd.Stderr.(*strings.Builder) - require.Eventuallyf(t, func() bool { // This will return errors until it connects to the agent, // they're mostly noise because until the agent starts running @@ -209,7 +237,14 @@ func TestContainerCMDWithAVeryLongStatePath(t *testing.T) { agentFixture, err := define.NewFixtureFromLocalBuild(t, define.Version()) require.NoError(t, err) - enrollmentToken := createPolicy(t, ctx, agentFixture, info) + _, enrollmentToken := createPolicy( + t, + ctx, + agentFixture, + info, + fmt.Sprintf("test-policy-enroll-%s", uuid.New().String()), + "") + env := []string{ "FLEET_ENROLL=1", "FLEET_URL=" + fleetURL, @@ -217,12 +252,11 @@ func TestContainerCMDWithAVeryLongStatePath(t *testing.T) { "STATE_PATH=" + tc.statePath, } - cmd := prepareContainerCMD(t, ctx, agentFixture, info, env) + cmd, agentOutput := prepareAgentCMD(t, ctx, agentFixture, []string{"container"}, env) t.Logf(">> running binary with: %v", cmd.Args) if err := cmd.Start(); err != nil { t.Fatalf("error running container cmd: %s", err) } - agentOutput := cmd.Stderr.(*strings.Builder) require.Eventuallyf(t, func() bool { // This will return errors until it connects to the agent, @@ -275,3 +309,180 @@ func withEnv(env []string) process.CmdOption { return nil } } + +func TestContainerCMDEventToStderr(t *testing.T) { + info := define.Require(t, define.Requirements{ + Stack: &define.Stack{}, + Local: false, + Sudo: true, + OS: []define.OS{ + {Type: define.Linux}, + }, + Group: "container", + }) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + agentFixture, err := define.NewFixtureFromLocalBuild(t, define.Version()) + require.NoError(t, err) + + _, outputID := createMockESOutput(t, info) + policyID, enrollmentAPIKey := createPolicy( + t, + ctx, + agentFixture, + info, + fmt.Sprintf("%s-%s", t.Name(), uuid.New().String()), + outputID) + + fleetURL, err := fleettools.DefaultURL(ctx, info.KibanaClient) + if err != nil { + t.Fatalf("could not get Fleet URL: %s", err) + } + + env := []string{ + "FLEET_ENROLL=1", + "FLEET_URL=" + fleetURL, + "FLEET_ENROLLMENT_TOKEN=" + enrollmentAPIKey, + "STATE_PATH=" + agentFixture.WorkDir(), + // That is what we're interested in testing + "EVENTS_TO_STDERR=true", + } + + cmd, agentOutput := prepareAgentCMD(t, ctx, agentFixture, []string{"container"}, env) + addLogIntegration(t, info, policyID, "/tmp/flog.log") + generateLogFile(t, "/tmp/flog.log", time.Second/2, 100) + + t.Logf(">> running binary with: %v", cmd.Args) + if err := cmd.Start(); err != nil { + t.Fatalf("error running container cmd: %s", err) + } + + assert.Eventuallyf(t, func() bool { + // This will return errors until it connects to the agent, + // they're mostly noise because until the agent starts running + // we will get connection errors. If the test fails + // the agent logs will be present in the error message + // which should help to explain why the agent was not + // healthy. + err := agentFixture.IsHealthy(ctx) + return err == nil + }, + 2*time.Minute, time.Second, + "Elastic-Agent did not report healthy. Agent status error: \"%v\", Agent logs\n%s", + err, agentOutput, + ) + + assert.Eventually(t, func() bool { + agentOutputStr := agentOutput.String() + scanner := bufio.NewScanner(strings.NewReader(agentOutputStr)) + for scanner.Scan() { + if strings.Contains(scanner.Text(), "Cannot index event publisher.Event") { + return true + } + } + + return false + }, 3*time.Minute, 10*time.Second, "cannot find events on stderr") +} + +func createMockESOutput(t *testing.T, info *define.Info) (string, string) { + mockesURL := startMockES(t) + createOutputBody := ` +{ + "id": "mock-es-%[1]s", + "name": "mock-es-%[1]s", + "type": "elasticsearch", + "is_default": false, + "hosts": [ + "%s" + ], + "preset": "latency" +} +` + // The API will return an error if the output ID/name contains an + // UUID substring, so we replace the '-' by '_' to keep the API happy. + outputUUID := strings.Replace(uuid.New().String(), "-", "_", -1) + bodyStr := fmt.Sprintf(createOutputBody, outputUUID, mockesURL) + bodyReader := strings.NewReader(bodyStr) + // THE URL IS MISSING + status, result, err := info.KibanaClient.Request(http.MethodPost, "/api/fleet/outputs", nil, nil, bodyReader) + if err != nil { + t.Fatalf("could execute request to create output: %#v, status: %d, result:\n%s\nBody:\n%s", err, status, string(result), bodyStr) + } + if status != http.StatusOK { + t.Fatalf("creating output failed. Status code %d, response\n:%s", status, string(result)) + } + + outputResp := struct { + Item struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"is_default"` + Hosts []string `json:"hosts"` + Preset string `json:"preset"` + IsDefaultMonitoring bool `json:"is_default_monitoring"` + } `json:"item"` + }{} + + if err := json.Unmarshal(result, &outputResp); err != nil { + t.Errorf("could not decode create output response: %s", err) + t.Logf("Response:\n%s", string(result)) + } + + return mockesURL, outputResp.Item.ID +} + +func addLogIntegration(t *testing.T, info *define.Info, policyID, logFilePath string) { + agentPolicyBuilder := strings.Builder{} + tmpl, err := template.New(t.Name() + "custom-log-policy").Parse(policyJSON) + if err != nil { + t.Fatalf("cannot parse template: %s", err) + } + + err = tmpl.Execute(&agentPolicyBuilder, policyVars{ + Name: "Log-Input-" + t.Name() + "-" + time.Now().Format(time.RFC3339), + PolicyID: policyID, + LogFilePath: logFilePath, + Dataset: "logs", + Namespace: "default", + }) + if err != nil { + t.Fatalf("could not render template: %s", err) + } + // We keep a copy of the policy for debugging prurposes + agentPolicy := agentPolicyBuilder.String() + + // Call Kibana to create the policy. + // Docs: https://www.elastic.co/guide/en/fleet/current/fleet-api-docs.html#create-integration-policy-api + resp, err := info.KibanaClient.Connection.Send( + http.MethodPost, + "/api/fleet/package_policies", + nil, + nil, + bytes.NewBufferString(agentPolicy)) + if err != nil { + t.Fatalf("could not execute request to Kibana/Fleet: %s", err) + } + if resp.StatusCode != http.StatusOK { + // On error dump the whole request response so we can easily spot + // what went wrong. + t.Errorf("received a non 200-OK when adding package to policy. "+ + "Status code: %d", resp.StatusCode) + respDump, err := httputil.DumpResponse(resp, true) + if err != nil { + t.Fatalf("could not dump error response from Kibana: %s", err) + } + // Make debugging as easy as possible + t.Log("================================================================================") + t.Log("Kibana error response:") + t.Log(string(respDump)) + t.Log("================================================================================") + t.Log("Rendered policy:") + t.Log(agentPolicy) + t.Log("================================================================================") + t.FailNow() + } +} diff --git a/testing/integration/event_logging_test.go b/testing/integration/event_logging_test.go new file mode 100644 index 00000000000..07483ff0d4b --- /dev/null +++ b/testing/integration/event_logging_test.go @@ -0,0 +1,405 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +//go:build integration + +package integration + +import ( + "bufio" + "bytes" + "context" + "fmt" + "net/http" + "net/http/httputil" + "os" + "path" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + atesting "github.com/elastic/elastic-agent/pkg/testing" + "github.com/elastic/elastic-agent/pkg/testing/define" + "github.com/elastic/elastic-agent/pkg/testing/tools/fleettools" + "github.com/elastic/elastic-agent/pkg/testing/tools/testcontext" +) + +var eventLogConfig = ` +outputs: + default: + type: elasticsearch + hosts: + - %s + protocol: http + preset: balanced + +inputs: + - type: filestream + id: your-input-id + streams: + - id: your-filestream-stream-id + data_stream: + dataset: generic + paths: + - %s + +# Disable monitoring so there are less Beats running and less logs being generated. +agent.monitoring: + enabled: false + logs: false + metrics: false + pprof.enabled: false + use_output: default + +# Needed if you already have an Elastic-Agent running on your machine +# That's very helpful for running the tests locally +agent.monitoring: + http: + enabled: false + port: 7002 +agent.grpc: + address: localhost + port: 7001 +` + +func TestEventLogFile(t *testing.T) { + _ = define.Require(t, define.Requirements{ + Group: Default, + Stack: &define.Stack{}, + Local: true, + Sudo: false, + }) + + ctx, cancel := testcontext.WithDeadline( + t, + context.Background(), + time.Now().Add(10*time.Minute)) + defer cancel() + + agentFixture, err := define.NewFixtureFromLocalBuild(t, define.Version()) + require.NoError(t, err) + + esURL := startMockES(t) + + logFilepath := path.Join(t.TempDir(), t.Name()) + generateLogFile(t, logFilepath, time.Millisecond*100, 1) + + cfg := fmt.Sprintf(eventLogConfig, esURL, logFilepath) + + if err := agentFixture.Prepare(ctx); err != nil { + t.Fatalf("cannot prepare Elastic-Agent fixture: %s", err) + } + + if err := agentFixture.Configure(ctx, []byte(cfg)); err != nil { + t.Fatalf("cannot configure Elastic-Agent fixture: %s", err) + } + + cmd, err := agentFixture.PrepareAgentCommand(ctx, nil) + if err != nil { + t.Fatalf("cannot prepare Elastic-Agent command: %s", err) + } + + output := strings.Builder{} + cmd.Stderr = &output + cmd.Stdout = &output + + if err := cmd.Start(); err != nil { + t.Fatalf("could not start Elastic-Agent: %s", err) + } + + // Make sure the Elastic-Agent process is not running before + // exiting the test + t.Cleanup(func() { + // Ignore the error because we cancelled the context, + // and that always returns an error + _ = cmd.Wait() + if t.Failed() { + t.Log("Elastic-Agent output:") + t.Log(output.String()) + } + }) + + // Now the Elastic-Agent is running, so validate the Event log file. + requireEventLogFileExistsWithData(t, agentFixture) + + // The diagnostics command is already tested by another test, + // here we just want to validate the events log behaviour + // extract the zip file into a temp folder + expectedLogFiles, expectedEventLogFiles := getLogFilenames( + t, + filepath.Join(agentFixture.WorkDir(), + "data", + "elastic-agent-*", + "logs")) + + collectDiagnosticsAndVeriflyLogs( + t, + ctx, + agentFixture, + []string{"diagnostics", "collect"}, + append(expectedLogFiles, expectedEventLogFiles...)) + + collectDiagnosticsAndVeriflyLogs( + t, + ctx, + agentFixture, + []string{"diagnostics", "collect", "--exclude-events"}, + expectedLogFiles) +} + +func TestEventLogOutputConfiguredViaFleet(t *testing.T) { + info := define.Require(t, define.Requirements{ + Stack: &define.Stack{}, + Local: false, + Sudo: true, + OS: []define.OS{ + {Type: define.Linux}, + }, + Group: "container", + }) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + agentFixture, err := define.NewFixtureFromLocalBuild(t, define.Version()) + require.NoError(t, err) + + _, outputID := createMockESOutput(t, info) + policyName := fmt.Sprintf("%s-%s", t.Name(), uuid.New().String()) + policyID, enrollmentAPIKey := createPolicy( + t, + ctx, + agentFixture, + info, + policyName, + outputID) + + fleetURL, err := fleettools.DefaultURL(ctx, info.KibanaClient) + if err != nil { + t.Fatalf("could not get Fleet URL: %s", err) + } + + enrollArgs := []string{ + "enroll", + "--force", + "--skip-daemon-reload", + "--url", + fleetURL, + "--enrollment-token", + enrollmentAPIKey, + } + + addLogIntegration(t, info, policyID, "/tmp/flog.log") + generateLogFile(t, "/tmp/flog.log", time.Second/2, 100) + + enrollCmd, err := agentFixture.PrepareAgentCommand(ctx, enrollArgs) + if err != nil { + t.Fatalf("could not prepare enroll command: %s", err) + } + if out, err := enrollCmd.CombinedOutput(); err != nil { + t.Fatalf("error enrolling Elastic-Agent: %s\nOutput:\n%s", err, string(out)) + } + + runAgentCMD, agentOutput := prepareAgentCMD(t, ctx, agentFixture, nil, nil) + if err := runAgentCMD.Start(); err != nil { + t.Fatalf("could not start Elastic-Agent: %s", err) + } + + assert.Eventuallyf(t, func() bool { + // This will return errors until it connects to the agent, + // they're mostly noise because until the agent starts running + // we will get connection errors. If the test fails + // the agent logs will be present in the error message + // which should help to explain why the agent was not + // healthy. + err := agentFixture.IsHealthy(ctx) + return err == nil + }, + 2*time.Minute, time.Second, + "Elastic-Agent did not report healthy. Agent status error: \"%v\", Agent logs\n%s", + err, agentOutput, + ) + + // The default behaviour is to log events to the events log file + // so ensure this is happening + requireEventLogFileExistsWithData(t, agentFixture) + + // Add a policy overwrite to change the events output to stderr + addOverwriteToPolicy(t, info, policyName, policyID) + + // Ensure Elastic-Agent is healthy after the policy change + assert.Eventuallyf(t, func() bool { + // This will return errors until it connects to the agent, + // they're mostly noise because until the agent starts running + // we will get connection errors. If the test fails + // the agent logs will be present in the error message + // which should help to explain why the agent was not + // healthy. + err := agentFixture.IsHealthy(ctx) + return err == nil + }, + 2*time.Minute, time.Second, + "Elastic-Agent did not report healthy after policy change. Agent status error: \"%v\", Agent logs\n%s", + err, agentOutput, + ) + + // Ensure the events logs are going to stderr + assert.Eventually(t, func() bool { + agentOutputStr := agentOutput.String() + scanner := bufio.NewScanner(strings.NewReader(agentOutputStr)) + for scanner.Scan() { + if strings.Contains(scanner.Text(), "Cannot index event publisher.Event") { + return true + } + } + + return false + }, 3*time.Minute, 10*time.Second, "cannot find events on stderr") + +} + +func addOverwriteToPolicy(t *testing.T, info *define.Info, policyName, policyID string) { + addLoggingOverwriteBody := fmt.Sprintf(` +{ + "name": "%s", + "namespace": "default", + "overrides": { + "agent": { + "logging": { + "event_data": { + "to_stderr": true, + "to_files": false + } + } + } + } +} +`, policyName) + resp, err := info.KibanaClient.Send( + http.MethodPut, + fmt.Sprintf("/api/fleet/agent_policies/%s", policyID), + nil, + nil, + bytes.NewBufferString(addLoggingOverwriteBody), + ) + if err != nil { + t.Fatalf("could not execute request to Kibana/Fleet: %s", err) + } + if resp.StatusCode != http.StatusOK { + // On error dump the whole request response so we can easily spot + // what went wrong. + t.Errorf("received a non 200-OK when adding overwrite to policy. "+ + "Status code: %d", resp.StatusCode) + respDump, err := httputil.DumpResponse(resp, true) + if err != nil { + t.Fatalf("could not dump error response from Kibana: %s", err) + } + // Make debugging as easy as possible + t.Log("================================================================================") + t.Log("Kibana error response:") + t.Log(string(respDump)) + t.FailNow() + } +} + +func requireEventLogFileExistsWithData(t *testing.T, agentFixture *atesting.Fixture) { + // Now the Elastic-Agent is running, so validate the Event log file. + // Because the path changes based on the Elastic-Agent version, we + // use glob to find the file + var logFileName string + require.Eventually(t, func() bool { + // We ignore this error because the folder might not be there. + // Once the folder and file are there, then this call should succeed + // and we can read the file. + glob := filepath.Join( + agentFixture.WorkDir(), + "data", "elastic-agent-*", "logs", "events", "*") + files, err := filepath.Glob(glob) + if err != nil { + t.Fatalf("could not scan for the events log file: %s", err) + } + + if len(files) == 1 { + logFileName = files[0] + return true + } + + return false + + }, time.Minute, time.Second, "could not find event log file") + + logEntryBytes, err := os.ReadFile(logFileName) + if err != nil { + t.Fatalf("cannot read file '%s': %s", logFileName, err) + } + + logEntry := string(logEntryBytes) + expectedStr := "Cannot index event publisher.Event" + if !strings.Contains(logEntry, expectedStr) { + t.Errorf( + "did not find the expected log entry ('%s') in the events log file", + expectedStr) + t.Log("Event log file contents:") + t.Log(logEntry) + } +} + +func collectDiagnosticsAndVeriflyLogs( + t *testing.T, + ctx context.Context, + agentFixture *atesting.Fixture, + cmd, + expectedFiles []string) { + + diagPath, err := agentFixture.ExecDiagnostics(ctx, cmd...) + if err != nil { + t.Fatalf("could not execute diagnostics excluding events log: %s", err) + } + + extractionDir := t.TempDir() + extractZipArchive(t, diagPath, extractionDir) + diagLogFiles, diagEventLogFiles := getLogFilenames( + t, + filepath.Join(extractionDir, "logs", "elastic-agent*")) + allLogs := append(diagLogFiles, diagEventLogFiles...) + + require.ElementsMatch( + t, + expectedFiles, + allLogs, + "expected: 'listA', got: 'listB'") +} + +func getLogFilenames( + t *testing.T, + basepath string, +) (logFiles, eventLogFiles []string) { + + logFilesGlob := filepath.Join(basepath, "*.ndjson") + logFilesPath, err := filepath.Glob(logFilesGlob) + if err != nil { + t.Fatalf("could not get log file names:%s", err) + } + + for _, f := range logFilesPath { + logFiles = append(logFiles, filepath.Base(f)) + } + + eventLogFilesGlob := filepath.Join(basepath, "events", "*.ndjson") + eventLogFilesPath, err := filepath.Glob(eventLogFilesGlob) + if err != nil { + t.Fatalf("could not get log file names:%s", err) + } + + for _, f := range eventLogFilesPath { + eventLogFiles = append(eventLogFiles, filepath.Base(f)) + } + + return logFiles, eventLogFiles +} diff --git a/testing/integration/logs_ingestion_test.go b/testing/integration/logs_ingestion_test.go index 718581d2035..42c1c976b27 100644 --- a/testing/integration/logs_ingestion_test.go +++ b/testing/integration/logs_ingestion_test.go @@ -15,7 +15,6 @@ import ( "net/http/httptest" "net/http/httputil" "os" - "path" "path/filepath" "regexp" "strings" @@ -246,220 +245,6 @@ func TestRpmLogIngestFleetManaged(t *testing.T) { }) } -var eventLogConfig = ` -outputs: - default: - type: elasticsearch - hosts: - - %s - protocol: http - preset: balanced - -inputs: - - type: filestream - id: your-input-id - streams: - - id: your-filestream-stream-id - data_stream: - dataset: generic - paths: - - %s - -# Disable monitoring so there are less Beats running and less logs being generated. -agent.monitoring: - enabled: false - logs: false - metrics: false - pprof.enabled: false - use_output: default - -# Needed if you already have an Elastic-Agent running on your machine -# That's very helpful for running the tests locally -agent.monitoring: - http: - enabled: false - port: 7002 -agent.grpc: - address: localhost - port: 7001 -` - -func TestEventLogFile(t *testing.T) { - _ = define.Require(t, define.Requirements{ - Group: Default, - Stack: &define.Stack{}, - Local: true, - Sudo: false, - }) - - ctx, cancel := testcontext.WithDeadline( - t, - context.Background(), - time.Now().Add(10*time.Minute)) - defer cancel() - - agentFixture, err := define.NewFixtureFromLocalBuild(t, define.Version()) - require.NoError(t, err) - - esURL := startMockES(t) - - logFilepath := path.Join(t.TempDir(), t.Name()) - generateLogFile(t, logFilepath, time.Millisecond*100, 1) - - cfg := fmt.Sprintf(eventLogConfig, esURL, logFilepath) - - if err := agentFixture.Prepare(ctx); err != nil { - t.Fatalf("cannot prepare Elastic-Agent fixture: %s", err) - } - - if err := agentFixture.Configure(ctx, []byte(cfg)); err != nil { - t.Fatalf("cannot configure Elastic-Agent fixture: %s", err) - } - - cmd, err := agentFixture.PrepareAgentCommand(ctx, nil) - if err != nil { - t.Fatalf("cannot prepare Elastic-Agent command: %s", err) - } - - output := strings.Builder{} - cmd.Stderr = &output - cmd.Stdout = &output - - if err := cmd.Start(); err != nil { - t.Fatalf("could not start Elastic-Agent: %s", err) - } - - // Make sure the Elastic-Agent process is not running before - // exiting the test - t.Cleanup(func() { - // Ignore the error because we cancelled the context, - // and that always returns an error - _ = cmd.Wait() - if t.Failed() { - t.Log("Elastic-Agent output:") - t.Log(output.String()) - } - }) - - // Now the Elastic-Agent is running, so validate the Event log file. - // Because the path changes based on the Elastic-Agent version, we - // use glob to find the file - var logFileName string - require.Eventually(t, func() bool { - // We ignore this error because the folder might not be there. - // Once the folder and file are there, then this call should succeed - // and we can read the file. - glob := filepath.Join( - agentFixture.WorkDir(), - "data", "elastic-agent-*", "logs", "events", "*") - files, err := filepath.Glob(glob) - if err != nil { - t.Fatalf("could not scan for the events log file: %s", err) - } - - if len(files) == 1 { - logFileName = files[0] - return true - } - - return false - - }, time.Minute, time.Second, "could not find event log file") - - logEntryBytes, err := os.ReadFile(logFileName) - if err != nil { - t.Fatalf("cannot read file '%s': %s", logFileName, err) - } - - logEntry := string(logEntryBytes) - expectedStr := "Cannot index event publisher.Event" - if !strings.Contains(logEntry, expectedStr) { - t.Errorf( - "did not find the expected log entry ('%s') in the events log file", - expectedStr) - t.Log("Event log file contents:") - t.Log(logEntry) - } - - // The diagnostics command is already tested by another test, - // here we just want to validate the events log behaviour - // extract the zip file into a temp folder - expectedLogFiles, expectedEventLogFiles := getLogFilenames( - t, - filepath.Join(agentFixture.WorkDir(), - "data", - "elastic-agent-*", - "logs")) - - collectDiagnosticsAndVeriflyLogs( - t, - ctx, - agentFixture, - []string{"diagnostics", "collect"}, - append(expectedLogFiles, expectedEventLogFiles...)) - - collectDiagnosticsAndVeriflyLogs( - t, - ctx, - agentFixture, - []string{"diagnostics", "collect", "--exclude-events"}, - expectedLogFiles) -} - -func collectDiagnosticsAndVeriflyLogs( - t *testing.T, - ctx context.Context, - agentFixture *atesting.Fixture, - cmd, - expectedFiles []string) { - - diagPath, err := agentFixture.ExecDiagnostics(ctx, cmd...) - if err != nil { - t.Fatalf("could not execute diagnostics excluding events log: %s", err) - } - - extractionDir := t.TempDir() - extractZipArchive(t, diagPath, extractionDir) - diagLogFiles, diagEventLogFiles := getLogFilenames( - t, - filepath.Join(extractionDir, "logs", "elastic-agent*")) - allLogs := append(diagLogFiles, diagEventLogFiles...) - - require.ElementsMatch( - t, - expectedFiles, - allLogs, - "expected: 'listA', got: 'listB'") -} - -func getLogFilenames( - t *testing.T, - basepath string, -) (logFiles, eventLogFiles []string) { - - logFilesGlob := filepath.Join(basepath, "*.ndjson") - logFilesPath, err := filepath.Glob(logFilesGlob) - if err != nil { - t.Fatalf("could not get log file names:%s", err) - } - - for _, f := range logFilesPath { - logFiles = append(logFiles, filepath.Base(f)) - } - - eventLogFilesGlob := filepath.Join(basepath, "events", "*.ndjson") - eventLogFilesPath, err := filepath.Glob(eventLogFilesGlob) - if err != nil { - t.Fatalf("could not get log file names:%s", err) - } - - for _, f := range eventLogFilesPath { - eventLogFiles = append(eventLogFiles, filepath.Base(f)) - } - - return logFiles, eventLogFiles -} - func startMockES(t *testing.T) string { registry := metrics.NewRegistry() uid := uuid.New()