diff --git a/heartbeat/autodiscover/builder/hints/config.go b/heartbeat/autodiscover/builder/hints/config.go new file mode 100644 index 000000000000..60cc5d6cdf4f --- /dev/null +++ b/heartbeat/autodiscover/builder/hints/config.go @@ -0,0 +1,30 @@ +// 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 hints + +type config struct { + Key string `config:"key"` + DefaultSchedule string `config:"defaults.schedule"` +} + +func defaultConfig() *config { + return &config{ + Key: "monitor", + DefaultSchedule: "@every 5s", + } +} diff --git a/heartbeat/autodiscover/builder/hints/monitors.go b/heartbeat/autodiscover/builder/hints/monitors.go new file mode 100644 index 000000000000..bb2a7d16ca70 --- /dev/null +++ b/heartbeat/autodiscover/builder/hints/monitors.go @@ -0,0 +1,205 @@ +// 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 hints + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "github.com/elastic/beats/libbeat/autodiscover" + "github.com/elastic/beats/libbeat/autodiscover/builder" + "github.com/elastic/beats/libbeat/autodiscover/template" + "github.com/elastic/beats/libbeat/common" + "github.com/elastic/beats/libbeat/common/bus" + "github.com/elastic/beats/libbeat/logp" +) + +func init() { + autodiscover.Registry.AddBuilder("hints", NewHeartbeatHints) +} + +const ( + montype = "type" + schedule = "schedule" + hosts = "hosts" + processors = "processors" +) + +type heartbeatHints struct { + config *config + logger *logp.Logger +} + +// NewHeartbeatHints builds a heartbeat hints builder +func NewHeartbeatHints(cfg *common.Config) (autodiscover.Builder, error) { + config := defaultConfig() + err := cfg.Unpack(config) + + if err != nil { + return nil, fmt.Errorf("unable to unpack hints config due to error: %v", err) + } + + return &heartbeatHints{config, logp.NewLogger("hints.builder")}, nil +} + +// Create config based on input hints in the bus event +func (hb *heartbeatHints) CreateConfig(event bus.Event) []*common.Config { + var hints common.MapStr + hIface, ok := event["hints"] + if ok { + hints, _ = hIface.(common.MapStr) + } + + monitorConfig := hb.getRawConfigs(hints) + + // If explicty disabled, return nothing + if builder.IsDisabled(hints, hb.config.Key) { + hb.logger.Warnf("heartbeat config disabled by hint: %+v", event) + return []*common.Config{} + } + + port, _ := common.TryToInt(event["port"]) + + host, _ := event["host"].(string) + if host == "" { + return []*common.Config{} + } + + if monitorConfig != nil { + configs := []*common.Config{} + for _, cfg := range monitorConfig { + if config, err := common.NewConfigFrom(cfg); err == nil { + configs = append(configs, config) + } + } + hb.logger.Debugf("generated config %+v", configs) + // Apply information in event to the template to generate the final config + return template.ApplyConfigTemplate(event, configs) + } + + tempCfg := common.MapStr{} + monitors := hb.getMonitors(hints) + + var configs []*common.Config + for _, monitor := range monitors { + // If a monitor doesn't have a schedule associated with it then default it. + if _, ok := monitor[schedule]; !ok { + monitor[schedule] = hb.config.DefaultSchedule + } + + if procs := hb.getProcessors(monitor); len(procs) != 0 { + monitor[processors] = procs + } + + h := hb.getHostsWithPort(monitor, port) + monitor[hosts] = h + + config, err := common.NewConfigFrom(monitor) + if err != nil { + hb.logger.Debugf("unable to create config from MapStr %+v", tempCfg) + return []*common.Config{} + } + hb.logger.Debugf("hints.builder", "generated config %+v", config) + configs = append(configs, config) + } + + // Apply information in event to the template to generate the final config + return template.ApplyConfigTemplate(event, configs) +} + +func (hb *heartbeatHints) getType(hints common.MapStr) common.MapStr { + return builder.GetHintMapStr(hints, hb.config.Key, montype) +} + +func (hb *heartbeatHints) getSchedule(hints common.MapStr) []string { + return builder.GetHintAsList(hints, hb.config.Key, schedule) +} + +func (hb *heartbeatHints) getRawConfigs(hints common.MapStr) []common.MapStr { + return builder.GetHintAsConfigs(hints, hb.config.Key) +} + +func (hb *heartbeatHints) getMonitors(hints common.MapStr) []common.MapStr { + raw := builder.GetHintMapStr(hints, hb.config.Key, "") + if raw == nil { + return nil + } + + var words, nums []string + + for key := range raw { + if _, err := strconv.Atoi(key); err != nil { + words = append(words, key) + continue + } else { + nums = append(nums, key) + } + } + + sort.Strings(nums) + + var configs []common.MapStr + for _, key := range nums { + rawCfg, _ := raw[key] + if config, ok := rawCfg.(common.MapStr); ok { + configs = append(configs, config) + } + } + + defaultMap := common.MapStr{} + for _, word := range words { + defaultMap[word] = raw[word] + } + + if len(defaultMap) != 0 { + configs = append(configs, defaultMap) + } + return configs +} + +func (hb *heartbeatHints) getProcessors(hints common.MapStr) []common.MapStr { + return builder.GetConfigs(hints, "", "processors") +} + +func (hb *heartbeatHints) getHostsWithPort(hints common.MapStr, port int) []string { + var result []string + thosts := builder.GetHintAsList(hints, "", hosts) + // Only pick hosts that have ${data.port} or the port on current event. This will make + // sure that incorrect meta mapping doesn't happen + for _, h := range thosts { + if strings.Contains(h, "data.port") || strings.Contains(h, fmt.Sprintf(":%d", port)) || + // Use the event that has no port config if there is a ${data.host}:9090 like input + (port == 0 && strings.Contains(h, "data.host")) { + result = append(result, h) + } else if port == 0 && !strings.Contains(h, ":") { + // For ICMP like use cases allow only host to be passed if there is no port + result = append(result, h) + } else { + hb.logger.Warn("unable to frame a host from input host: %s", h) + } + } + + if len(thosts) > 0 && len(result) == 0 { + hb.logger.Debugf("no hosts selected for port %d with hints: %+v", port, thosts) + return nil + } + + return result +} diff --git a/heartbeat/autodiscover/builder/hints/monitors_test.go b/heartbeat/autodiscover/builder/hints/monitors_test.go new file mode 100644 index 000000000000..d3d37f990908 --- /dev/null +++ b/heartbeat/autodiscover/builder/hints/monitors_test.go @@ -0,0 +1,218 @@ +// 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 hints + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/elastic/beats/libbeat/common" + "github.com/elastic/beats/libbeat/common/bus" + "github.com/elastic/beats/libbeat/logp" +) + +func TestGenerateHints(t *testing.T) { + tests := []struct { + message string + event bus.Event + len int + result common.MapStr + }{ + { + message: "Empty event hints should return empty config", + event: bus.Event{ + "host": "1.2.3.4", + "kubernetes": common.MapStr{ + "container": common.MapStr{ + "name": "foobar", + "id": "abc", + }, + }, + "docker": common.MapStr{ + "container": common.MapStr{ + "name": "foobar", + "id": "abc", + }, + }, + }, + len: 0, + result: common.MapStr{}, + }, + { + message: "Hints without host should return nothing", + event: bus.Event{ + "hints": common.MapStr{ + "monitor": common.MapStr{ + "type": "icmp", + }, + }, + }, + len: 0, + result: common.MapStr{}, + }, + { + message: "Hints without matching port should return nothing in the hosts section", + event: bus.Event{ + "host": "1.2.3.4", + "port": 9090, + "hints": common.MapStr{ + "monitor": common.MapStr{ + "type": "icmp", + "hosts": "${data.host}:8888", + }, + }, + }, + len: 1, + result: common.MapStr{ + "schedule": "@every 5s", + "type": "icmp", + }, + }, + { + message: "Hints with multiple hosts return only the matching one", + event: bus.Event{ + "host": "1.2.3.4", + "port": 9090, + "hints": common.MapStr{ + "monitor": common.MapStr{ + "type": "icmp", + "hosts": "${data.host}:8888,${data.host}:9090", + }, + }, + }, + len: 1, + result: common.MapStr{ + "type": "icmp", + "schedule": "@every 5s", + "hosts": []interface{}{"1.2.3.4:9090"}, + }, + }, + { + message: "Hints with multiple hosts return only the one with the template", + event: bus.Event{ + "host": "1.2.3.4", + "port": 9090, + "hints": common.MapStr{ + "monitor": common.MapStr{ + "type": "icmp", + "hosts": "${data.host}:8888,${data.host}:${data.port}", + }, + }, + }, + len: 1, + result: common.MapStr{ + "type": "icmp", + "schedule": "@every 5s", + "hosts": []interface{}{"1.2.3.4:9090"}, + }, + }, + { + message: "Monitor defined in monitors as a JSON string should return a config", + event: bus.Event{ + "host": "1.2.3.4", + "hints": common.MapStr{ + "monitor": common.MapStr{ + "raw": "{\"enabled\":true,\"type\":\"icmp\",\"schedule\":\"@every 20s\",\"timeout\":\"3s\"}", + }, + }, + }, + len: 1, + result: common.MapStr{ + "type": "icmp", + "timeout": "3s", + "schedule": "@every 20s", + "enabled": true, + }, + }, + { + message: "Monitor with processor config must return an module having the processor defined", + event: bus.Event{ + "host": "1.2.3.4", + "port": 9090, + "hints": common.MapStr{ + "monitor": common.MapStr{ + "type": "icmp", + "hosts": "${data.host}:9090", + "processors": common.MapStr{ + "add_locale": common.MapStr{ + "abbrevation": "MST", + }, + }, + }, + }, + }, + len: 1, + result: common.MapStr{ + "type": "icmp", + "hosts": []interface{}{"1.2.3.4:9090"}, + "schedule": "@every 5s", + "processors": []interface{}{ + map[string]interface{}{ + "add_locale": map[string]interface{}{ + "abbrevation": "MST", + }, + }, + }, + }, + }, + { + message: "Hints with multiple monitors should return multiple", + event: bus.Event{ + "host": "1.2.3.4", + "port": 9090, + "hints": common.MapStr{ + "monitor": common.MapStr{ + "1": common.MapStr{ + "type": "icmp", + "hosts": "${data.host}:8888,${data.host}:9090", + }, + "2": common.MapStr{ + "type": "icmp", + "hosts": "${data.host}:8888,${data.host}:9090", + }, + }, + }, + }, + len: 2, + result: common.MapStr{ + "type": "icmp", + "schedule": "@every 5s", + "hosts": []interface{}{"1.2.3.4:9090"}, + }, + }, + } + for _, test := range tests { + + m := heartbeatHints{ + config: defaultConfig(), + logger: logp.NewLogger("hints.builder"), + } + cfgs := m.CreateConfig(test.event) + assert.Equal(t, len(cfgs), test.len, test.message) + + if len(cfgs) != 0 { + config := common.MapStr{} + err := cfgs[0].Unpack(&config) + assert.Nil(t, err, test.message) + + assert.Equal(t, test.result, config, test.message) + } + + } +} diff --git a/heartbeat/autodiscover/include.go b/heartbeat/autodiscover/include.go new file mode 100644 index 000000000000..cd4bf8bf0b62 --- /dev/null +++ b/heartbeat/autodiscover/include.go @@ -0,0 +1,23 @@ +// 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 autodiscover + +import ( + // include all heartbeat specific builders + _ "github.com/elastic/beats/heartbeat/autodiscover/builder/hints" +) diff --git a/heartbeat/cmd/root.go b/heartbeat/cmd/root.go index 99cd9e5d4be9..d86e4291aa83 100644 --- a/heartbeat/cmd/root.go +++ b/heartbeat/cmd/root.go @@ -20,6 +20,7 @@ package cmd import ( "fmt" // register default heartbeat monitors + _ "github.com/elastic/beats/heartbeat/autodiscover" "github.com/elastic/beats/heartbeat/beater" _ "github.com/elastic/beats/heartbeat/monitors/defaults" cmd "github.com/elastic/beats/libbeat/cmd" diff --git a/libbeat/autodiscover/builder/helper.go b/libbeat/autodiscover/builder/helper.go index d9c1235f5fc8..e9c00abb340a 100644 --- a/libbeat/autodiscover/builder/helper.go +++ b/libbeat/autodiscover/builder/helper.go @@ -43,7 +43,13 @@ func GetContainerName(container common.MapStr) string { // GetHintString takes a hint and returns its value as a string func GetHintString(hints common.MapStr, key, config string) string { - if iface, err := hints.GetValue(fmt.Sprintf("%s.%s", key, config)); err == nil { + base := config + if base == "" { + base = key + } else if key != "" { + base = fmt.Sprint(key, ".", config) + } + if iface, err := hints.GetValue(base); err == nil { if str, ok := iface.(string); ok { return str } @@ -54,7 +60,13 @@ func GetHintString(hints common.MapStr, key, config string) string { // GetHintMapStr takes a hint and returns a MapStr func GetHintMapStr(hints common.MapStr, key, config string) common.MapStr { - if iface, err := hints.GetValue(fmt.Sprintf("%s.%s", key, config)); err == nil { + base := config + if base == "" { + base = key + } else if key != "" { + base = fmt.Sprint(key, ".", config) + } + if iface, err := hints.GetValue(base); err == nil { if mapstr, ok := iface.(common.MapStr); ok { return mapstr }