// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package beater import ( "flag" "fmt" "strings" "github.com/elastic/beats/libbeat/common/reload" "github.com/joeshaw/multierror" "github.com/pkg/errors" "github.com/elastic/beats/libbeat/autodiscover" "github.com/elastic/beats/libbeat/beat" "github.com/elastic/beats/libbeat/cfgfile" "github.com/elastic/beats/libbeat/common" "github.com/elastic/beats/libbeat/common/cfgwarn" "github.com/elastic/beats/libbeat/kibana" "github.com/elastic/beats/libbeat/logp" "github.com/elastic/beats/libbeat/management" "github.com/elastic/beats/libbeat/monitoring" "github.com/elastic/beats/libbeat/outputs/elasticsearch" fbautodiscover "github.com/elastic/beats/filebeat/autodiscover" "github.com/elastic/beats/filebeat/channel" cfg "github.com/elastic/beats/filebeat/config" "github.com/elastic/beats/filebeat/crawler" "github.com/elastic/beats/filebeat/fileset" "github.com/elastic/beats/filebeat/registrar" // Add filebeat level processors _ "github.com/elastic/beats/filebeat/processor/add_kubernetes_metadata" ) const pipelinesWarning = "Filebeat is unable to load the Ingest Node pipelines for the configured" + " modules because the Elasticsearch output is not configured/enabled. If you have" + " already loaded the Ingest Node pipelines or are using Logstash pipelines, you" + " can ignore this warning." var ( once = flag.Bool("once", false, "Run filebeat only once until all harvesters reach EOF") ) // Filebeat is a beater object. Contains all objects needed to run the beat type Filebeat struct { config *cfg.Config moduleRegistry *fileset.ModuleRegistry done chan struct{} } // New creates a new Filebeat pointer instance. func New(b *beat.Beat, rawConfig *common.Config) (beat.Beater, error) { config := cfg.DefaultConfig if err := rawConfig.Unpack(&config); err != nil { return nil, fmt.Errorf("Error reading config file: %v", err) } err := cfgwarn.CheckRemoved5xSettings(rawConfig, "spool_size", "publish_async", "idle_timeout") if err != nil { return nil, err } if len(config.Prospectors) > 0 { cfgwarn.Deprecate("7.0.0", "prospectors are deprecated, Use `inputs` instead.") if len(config.Inputs) > 0 { return nil, fmt.Errorf("prospectors and inputs used in the configuration file, define only inputs not both") } config.Inputs = config.Prospectors } if config.ConfigProspector != nil { cfgwarn.Deprecate("7.0.0", "config.prospectors are deprecated, Use `config.inputs` instead.") if config.ConfigInput != nil { return nil, fmt.Errorf("config.prospectors and config.inputs used in the configuration file, define only config.inputs not both") } config.ConfigInput = config.ConfigProspector } moduleRegistry, err := fileset.NewModuleRegistry(config.Modules, b.Info.Version, true) if err != nil { return nil, err } if !moduleRegistry.Empty() { logp.Info("Enabled modules/filesets: %s", moduleRegistry.InfoString()) } moduleInputs, err := moduleRegistry.GetInputConfigs() if err != nil { return nil, err } if err := config.FetchConfigs(); err != nil { return nil, err } // Add inputs created by the modules config.Inputs = append(config.Inputs, moduleInputs...) enabledInputs := config.ListEnabledInputs() var haveEnabledInputs bool if len(enabledInputs) > 0 { haveEnabledInputs = true } if !config.ConfigInput.Enabled() && !config.ConfigModules.Enabled() && !haveEnabledInputs && config.Autodiscover == nil && !b.ConfigManager.Enabled() { if !b.InSetupCmd { return nil, errors.New("no modules or inputs enabled and configuration reloading disabled. What files do you want me to watch?") } // in the `setup` command, log this only as a warning logp.Warn("Setup called, but no modules enabled.") } if *once && config.ConfigInput.Enabled() && config.ConfigModules.Enabled() { return nil, errors.New("input configs and -once cannot be used together") } if config.IsInputEnabled("stdin") && len(enabledInputs) > 1 { return nil, fmt.Errorf("stdin requires to be run in exclusive mode, configured inputs: %s", strings.Join(enabledInputs, ", ")) } fb := &Filebeat{ done: make(chan struct{}), config: &config, moduleRegistry: moduleRegistry, } // register `setup` callback for ML jobs b.SetupMLCallback = func(b *beat.Beat, kibanaConfig *common.Config) error { return fb.loadModulesML(b, kibanaConfig) } err = fb.setupPipelineLoaderCallback(b) if err != nil { return nil, err } return fb, nil } func (fb *Filebeat) setupPipelineLoaderCallback(b *beat.Beat) error { if !fb.moduleRegistry.Empty() { overwritePipelines := fb.config.OverwritePipelines if b.InSetupCmd { overwritePipelines = true } b.OverwritePipelinesCallback = func(esConfig *common.Config) error { esClient, err := elasticsearch.NewConnectedClient(esConfig) if err != nil { return err } return fb.moduleRegistry.LoadPipelines(esClient, overwritePipelines) } } return nil } // loadModulesPipelines is called when modules are configured to do the initial // setup. func (fb *Filebeat) loadModulesPipelines(b *beat.Beat) error { if b.Config.Output.Name() != "elasticsearch" { logp.Warn(pipelinesWarning) return nil } overwritePipelines := fb.config.OverwritePipelines if b.InSetupCmd { overwritePipelines = true } // register pipeline loading to happen every time a new ES connection is // established callback := func(esClient *elasticsearch.Client) error { return fb.moduleRegistry.LoadPipelines(esClient, overwritePipelines) } _, err := elasticsearch.RegisterConnectCallback(callback) return err } func (fb *Filebeat) loadModulesML(b *beat.Beat, kibanaConfig *common.Config) error { var errs multierror.Errors logp.Debug("machine-learning", "Setting up ML jobs for modules") if b.Config.Output.Name() != "elasticsearch" { logp.Warn("Filebeat is unable to load the Xpack Machine Learning configurations for the" + " modules because the Elasticsearch output is not configured/enabled.") return nil } esConfig := b.Config.Output.Config() esClient, err := elasticsearch.NewConnectedClient(esConfig) if err != nil { return errors.Errorf("Error creating Elasticsearch client: %v", err) } if kibanaConfig == nil { kibanaConfig = common.NewConfig() } if esConfig.Enabled() { username, _ := esConfig.String("username", -1) password, _ := esConfig.String("password", -1) if !kibanaConfig.HasField("username") && username != "" { kibanaConfig.SetString("username", -1, username) } if !kibanaConfig.HasField("password") && password != "" { kibanaConfig.SetString("password", -1, password) } } kibanaClient, err := kibana.NewKibanaClient(kibanaConfig) if err != nil { return errors.Errorf("Error creating Kibana client: %v", err) } kibanaVersion, err := common.NewVersion(kibanaClient.GetVersion()) if err != nil { return errors.Errorf("Error checking Kibana version: %v", err) } if err := setupMLBasedOnVersion(fb.moduleRegistry, esClient, kibanaClient, kibanaVersion); err != nil { errs = append(errs, err) } // Add dynamic modules.d if fb.config.ConfigModules.Enabled() { config := cfgfile.DefaultDynamicConfig fb.config.ConfigModules.Unpack(&config) modulesManager, err := cfgfile.NewGlobManager(config.Path, ".yml", ".disabled") if err != nil { return errors.Wrap(err, "initialization error") } for _, file := range modulesManager.ListEnabled() { confs, err := cfgfile.LoadList(file.Path) if err != nil { errs = append(errs, errors.Wrap(err, "error loading config file")) continue } set, err := fileset.NewModuleRegistry(confs, "", false) if err != nil { errs = append(errs, err) continue } if err := setupMLBasedOnVersion(set, esClient, kibanaClient, kibanaVersion); err != nil { errs = append(errs, err) } } } return errs.Err() } func setupMLBasedOnVersion(reg *fileset.ModuleRegistry, esClient *elasticsearch.Client, kibanaClient *kibana.Client, kibanaVersion *common.Version) error { if isElasticsearchLoads(kibanaVersion) { return reg.LoadML(esClient) } return reg.SetupML(esClient, kibanaClient) } func isElasticsearchLoads(kibanaVersion *common.Version) bool { if kibanaVersion.Major < 6 || kibanaVersion.Major == 6 && kibanaVersion.Minor < 1 { return true } return false } // Run allows the beater to be run as a beat. func (fb *Filebeat) Run(b *beat.Beat) error { var err error config := fb.config if !fb.moduleRegistry.Empty() { err = fb.loadModulesPipelines(b) if err != nil { return err } } waitFinished := newSignalWait() waitEvents := newSignalWait() // count active events for waiting on shutdown wgEvents := &eventCounter{ count: monitoring.NewInt(nil, "filebeat.events.active"), added: monitoring.NewUint(nil, "filebeat.events.added"), done: monitoring.NewUint(nil, "filebeat.events.done"), } finishedLogger := newFinishedLogger(wgEvents) // Setup registrar to persist state registrar, err := registrar.New(config.RegistryFile, config.RegistryFilePermissions, config.RegistryFlush, finishedLogger) if err != nil { logp.Err("Could not init registrar: %v", err) return err } // Make sure all events that were published in registrarChannel := newRegistrarLogger(registrar) err = b.Publisher.SetACKHandler(beat.PipelineACKHandler{ ACKEvents: newEventACKer(finishedLogger, registrarChannel).ackEvents, }) if err != nil { logp.Err("Failed to install the registry with the publisher pipeline: %v", err) return err } outDone := make(chan struct{}) // outDone closes down all active pipeline connections crawler, err := crawler.New( channel.NewOutletFactory(outDone, wgEvents).Create, config.Inputs, b.Info.Version, fb.done, *once) if err != nil { logp.Err("Could not init crawler: %v", err) return err } // The order of starting and stopping is important. Stopping is inverted to the starting order. // The current order is: registrar, publisher, spooler, crawler // That means, crawler is stopped first. // Start the registrar err = registrar.Start() if err != nil { return fmt.Errorf("Could not start registrar: %v", err) } // Stopping registrar will write last state defer registrar.Stop() // Stopping publisher (might potentially drop items) defer func() { // Closes first the registrar logger to make sure not more events arrive at the registrar // registrarChannel must be closed first to potentially unblock (pretty unlikely) the publisher registrarChannel.Close() close(outDone) // finally close all active connections to publisher pipeline }() // Wait for all events to be processed or timeout defer waitEvents.Wait() // Create a ES connection factory for dynamic modules pipeline loading var pipelineLoaderFactory fileset.PipelineLoaderFactory if b.Config.Output.Name() == "elasticsearch" { pipelineLoaderFactory = newPipelineLoaderFactory(b.Config.Output.Config()) } else { logp.Warn(pipelinesWarning) } if config.OverwritePipelines { logp.Debug("modules", "Existing Ingest pipelines will be updated") } err = crawler.Start(b.Publisher, registrar, config.ConfigInput, config.ConfigModules, pipelineLoaderFactory, config.OverwritePipelines) if err != nil { crawler.Stop() return err } // If run once, add crawler completion check as alternative to done signal if *once { runOnce := func() { logp.Info("Running filebeat once. Waiting for completion ...") crawler.WaitForCompletion() logp.Info("All data collection completed. Shutting down.") } waitFinished.Add(runOnce) } // Register reloadable list of inputs and modules inputs := cfgfile.NewRunnerList(management.DebugK, crawler.InputsFactory, b.Publisher) reload.Register.MustRegisterList("filebeat.inputs", inputs) modules := cfgfile.NewRunnerList(management.DebugK, crawler.ModulesFactory, b.Publisher) reload.Register.MustRegisterList("filebeat.modules", modules) var adiscover *autodiscover.Autodiscover if fb.config.Autodiscover != nil { adapter := fbautodiscover.NewAutodiscoverAdapter(crawler.InputsFactory, crawler.ModulesFactory) adiscover, err = autodiscover.NewAutodiscover("filebeat", b.Publisher, adapter, config.Autodiscover) if err != nil { return err } } adiscover.Start() // Add done channel to wait for shutdown signal waitFinished.AddChan(fb.done) waitFinished.Wait() // Stop reloadable lists, autodiscover -> Stop crawler -> stop inputs -> stop harvesters // Note: waiting for crawlers to stop here in order to install wgEvents.Wait // after all events have been enqueued for publishing. Otherwise wgEvents.Wait // or publisher might panic due to concurrent updates. inputs.Stop() modules.Stop() adiscover.Stop() crawler.Stop() timeout := fb.config.ShutdownTimeout // Checks if on shutdown it should wait for all events to be published waitPublished := fb.config.ShutdownTimeout > 0 || *once if waitPublished { // Wait for registrar to finish writing registry waitEvents.Add(withLog(wgEvents.Wait, "Continue shutdown: All enqueued events being published.")) // Wait for either timeout or all events having been ACKed by outputs. if fb.config.ShutdownTimeout > 0 { logp.Info("Shutdown output timer started. Waiting for max %v.", timeout) waitEvents.Add(withLog(waitDuration(timeout), "Continue shutdown: Time out waiting for events being published.")) } else { waitEvents.AddChan(fb.done) } } return nil } // Stop is called on exit to stop the crawling, spooling and registration processes. func (fb *Filebeat) Stop() { logp.Info("Stopping filebeat") // Stop Filebeat close(fb.done) } // Create a new pipeline loader (es client) factory func newPipelineLoaderFactory(esConfig *common.Config) fileset.PipelineLoaderFactory { pipelineLoaderFactory := func() (fileset.PipelineLoader, error) { esClient, err := elasticsearch.NewConnectedClient(esConfig) if err != nil { return nil, errors.Wrap(err, "Error creating Elasticsearch client") } return esClient, nil } return pipelineLoaderFactory }