Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the option to generate the template into a file #4323

Merged
merged 4 commits into from
May 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ https://github.com/elastic/beats/compare/v6.0.0-alpha1...master[Check the HEAD d
*Affecting all Beats*

- Added the possibility to set Elasticsearch mapping template settings from the Beat configuration file. {pull}4284[4284] {pull}4317[4317]
- Add the option to write the generated Elasticsearch mapping template into a file. {pull}4323[4323]

*Filebeat*

Expand Down
26 changes: 24 additions & 2 deletions libbeat/beat/beat.go
Original file line number Diff line number Diff line change
Expand Up @@ -469,8 +469,30 @@ func (b *Beat) registerTemplateLoading() error {
}
}

esConfig := b.Config.Output["elasticsearch"]
// Check if outputting to file is enabled, and output to file if it is
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment confused me at first because the check for the file only happens later, but I see you meant it for the "complete" block

if b.Config.Template != nil && b.Config.Template.Enabled() {
var cfg template.TemplateConfig
err := b.Config.Template.Unpack(&cfg)
if err != nil {
return fmt.Errorf("unpacking template config fails: %v", err)
}
if len(cfg.OutputToFile.Path) > 0 {
// output to file is enabled
loader, err := template.NewLoader(b.Config.Template, nil, b.Info)
if err != nil {
return fmt.Errorf("Error creating Elasticsearch template loader: %v", err)
}
err = loader.Generate()
if err != nil {
return fmt.Errorf("Error generating template: %v", err)
}

// XXX: Should we kill the Beat here or just continue?
return fmt.Errorf("Stopping after successfully writing the template to the file.")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[golint] reported by reviewdog 🐶
error strings should not be capitalized or end with punctuation or a newline

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, dear reviewdog, we know that the error is final, so the punctuation looks better to the user.

}
}

esConfig := b.Config.Output["elasticsearch"]
// Loads template by default if esOutput is enabled
if (b.Config.Template == nil && esConfig.Enabled()) || (b.Config.Template != nil && b.Config.Template.Enabled()) {
if esConfig == nil || !esConfig.Enabled() {
Expand All @@ -487,7 +509,7 @@ func (b *Beat) registerTemplateLoading() error {

loader, err := template.NewLoader(b.Config.Template, esClient, b.Info)
if err != nil {
return fmt.Errorf("Error creating Elasticsearch template: %v", err)
return fmt.Errorf("Error creating Elasticsearch template loader: %v", err)
}

err = loader.Load()
Expand Down
22 changes: 22 additions & 0 deletions libbeat/docs/template-config.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,25 @@ setup.template.overwrite: false
setup.template.settings:
_source.enabled: false
----------------------------------------------------------------------

*`output_to_file.path`*:: If this option is set, {beatname_uc} generates the Elasticsearch template
JSON object and writes into a file at the specified path. Immediately after writing the file,
{beatname_uc} exists with the exit code 1.

For example, you can generate a template file ready to be uploaded to Elasticsearch like this:

["source","yaml",subs="attributes,callouts"]
----------------------------------------------------------------------
./{beatname_lc} -e -E "setup.template.output_to_file.path={beatname_lc}.template.json"
----------------------------------------------------------------------

*`output_to_file.version`*:: The Elasticsearch version for which to generate the template file. By
default, the {beatname_uc} version is used. This setting is only used if `output_to_file.path` is
also set.

For example, the following generates a template file for Elasticsearch 5.4:

["source","yaml",subs="attributes,callouts"]
----------------------------------------------------------------------
./{beatname_lc} -e -E "setup.template.output_to_file.path={beatname_lc}.template.json" -E "setup.template.output_to_file.version=5.4.0"
----------------------------------------------------------------------
9 changes: 8 additions & 1 deletion libbeat/template/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ type TemplateConfig struct {
Name string `config:"name"`
Fields string `config:"fields"`
Overwrite bool `config:"overwrite"`
OutputToFile string `config:"output_to_file"`
Settings templateSettings `config:"settings"`
OutputToFile OutputToFile `config:"output_to_file"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not too much of a fan of the config name, but well :-)

}

// OutputToFile contains the configuration options for generating
// and writing the template into a file.
type OutputToFile struct {
Path string `config:"path"`
Version string `config:"version"`
}

type templateSettings struct {
Expand Down
41 changes: 40 additions & 1 deletion libbeat/template/load.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package template

import (
"encoding/json"
"fmt"
"io/ioutil"

"github.com/elastic/beats/libbeat/common"
"github.com/elastic/beats/libbeat/logp"
Expand Down Expand Up @@ -38,7 +40,7 @@ func NewLoader(cfg *common.Config, client ESClient, beatInfo common.BeatInfo) (*
}, nil
}

// loadTemplate checks if the index mapping template should be loaded
// Load checks if the index mapping template should be loaded
// In case the template is not already loaded or overwriting is enabled, the
// template is written to index
func (l *Loader) Load() error {
Expand Down Expand Up @@ -80,6 +82,43 @@ func (l *Loader) Load() error {
return nil
}

// Generate generates the template and writes it to a file based on the configuration
// from `output_to_file`.
func (l *Loader) Generate() error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I didn't realize we have that utility already.

if l.config.OutputToFile.Version == "" {
l.config.OutputToFile.Version = l.beatInfo.Version
}

if l.config.Name == "" {
l.config.Name = l.beatInfo.Beat
}

tmpl, err := New(l.beatInfo.Version, l.config.OutputToFile.Version, l.config.Name, l.config.Settings)
if err != nil {
return fmt.Errorf("error creating template instance: %v", err)
}

fieldsPath := paths.Resolve(paths.Config, l.config.Fields)

output, err := tmpl.Load(fieldsPath)
if err != nil {
return fmt.Errorf("error creating template from file %s: %v", fieldsPath, err)
}

jsonBytes, err := json.MarshalIndent(output, "", " ")
if err != nil {
return fmt.Errorf("error marshaling template: %v", err)
}

err = ioutil.WriteFile(l.config.OutputToFile.Path, jsonBytes, 0644)
if err != nil {
return fmt.Errorf("error writing to file %s: %v", l.config.OutputToFile.Path, err)
}

logp.Info("Template for Elasticsearch %s written to: %s", l.config.OutputToFile.Version, l.config.OutputToFile.Path)
return nil
}

// LoadTemplate loads a template into Elasticsearch overwriting the existing
// template if it exists. If you wish to not overwrite an existing template
// then use CheckTemplate prior to calling this method.
Expand Down
112 changes: 112 additions & 0 deletions libbeat/template/load_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// +build !integration

package template

import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"testing"

"github.com/elastic/beats/libbeat/common"
"github.com/elastic/beats/libbeat/version"
"github.com/stretchr/testify/assert"
)

func TestGenerateTemplate(t *testing.T) {

// Load template
absPath, err := filepath.Abs("../")
assert.NotNil(t, absPath)
assert.Nil(t, err)

beatInfo := common.BeatInfo{
Beat: "testbeat",
Version: version.GetDefaultVersion(),
}

dir, err := ioutil.TempDir("", "test-template")
assert.NoError(t, err)
defer os.RemoveAll(dir)
path := filepath.Join(dir, "template.json")

config := newConfigFrom(t, TemplateConfig{
Enabled: true,
Fields: absPath + "/fields.yml",
OutputToFile: OutputToFile{
Path: path,
},
})

loader, err := NewLoader(config, nil, beatInfo)
assert.NoError(t, err)

err = loader.Generate()
assert.NoError(t, err)

// Read it back to check it
fp, err := os.Open(path)
assert.NoError(t, err)
jsonParser := json.NewDecoder(fp)
var parsed common.MapStr
err = jsonParser.Decode(&parsed)
assert.NoError(t, err)

val, err := parsed.GetValue("mappings._default_._meta.version")
assert.NoError(t, err)
assert.Equal(t, val.(string), version.GetDefaultVersion())

}

func TestGenerateTemplateWithVersion(t *testing.T) {

// Load template
absPath, err := filepath.Abs("../")
assert.NotNil(t, absPath)
assert.Nil(t, err)

beatInfo := common.BeatInfo{
Beat: "testbeat",
Version: version.GetDefaultVersion(),
}

dir, err := ioutil.TempDir("", "test-template")
assert.NoError(t, err)
defer os.RemoveAll(dir)
path := filepath.Join(dir, "template.json")

config := newConfigFrom(t, TemplateConfig{
Enabled: true,
Fields: absPath + "/fields.yml",
OutputToFile: OutputToFile{
Path: path,
Version: "2.4.0",
},
})

loader, err := NewLoader(config, nil, beatInfo)
assert.NoError(t, err)

err = loader.Generate()
assert.NoError(t, err)

// Read it back to check it
fp, err := os.Open(path)
assert.NoError(t, err)
jsonParser := json.NewDecoder(fp)
var parsed common.MapStr
err = jsonParser.Decode(&parsed)
assert.NoError(t, err)

// check a setting specific to that version
val, err := parsed.GetValue("mappings._default_._all.norms.enabled")
assert.NoError(t, err)
assert.Equal(t, val.(bool), false)
}

func newConfigFrom(t *testing.T, from interface{}) *common.Config {
cfg, err := common.NewConfigFrom(from)
assert.NoError(t, err)
return cfg
}
7 changes: 7 additions & 0 deletions libbeat/tests/system/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,11 @@ def setUpClass(self):
self.beat_name = "mockbeat"
self.beat_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))
self.test_binary = self.beat_path + "/libbeat.test"
self.beats = [
"filebeat",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a beats specifics dependency in libbeat.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, only needed for those skipped tets for now, I can delete them.

"heartbeat",
"metricbeat",
"packetbeat",
"winlogbeat"
]
super(BaseTest, self).setUpClass()
10 changes: 3 additions & 7 deletions libbeat/tests/system/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import subprocess
import unittest
import re
from nose.plugins.skip import Skip, SkipTest
from nose.plugins.skip import SkipTest


INTEGRATION_TESTS = os.environ.get('INTEGRATION_TESTS', False)
Expand All @@ -20,9 +20,7 @@ def test_load_dashboard(self):
"""
Test loading dashboards for all beats
"""
beats = ["metricbeat", "packetbeat", "filebeat", "winlogbeat"]

for beat in beats:
for beat in self.beats:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

be aware that these tests are currently skipped.

command = "go run ../../../dev-tools/cmd/import_dashboards/import_dashboards.go -es http://" + \
self.get_elasticsearch_host() + " -dir ../../../" + beat + "/_meta/kibana"

Expand Down Expand Up @@ -51,9 +49,7 @@ def test_export_dashboard(self):
# In addition, this test should not write to the beats directory but to a
# temporary directory and check the files there.

beats = ["metricbeat", "packetbeat", "filebeat", "winlogbeat"]

for beat in beats:
for beat in self.beats:
if os.name == "nt":
path = "..\..\..\\" + beat + "\etc\kibana"
else:
Expand Down
47 changes: 47 additions & 0 deletions libbeat/tests/system/test_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from base import BaseTest

import os
import json


class Test(BaseTest):

def test_generate_templates(self):
"""
Generates templates from other Beats.
"""
self.render_config_template()

output_json = os.path.join(self.working_dir, "template.json")
fields_yml = "../../../../fields.yml"

exit_code = self.run_beat(extra_args=[
"-E", "setup.template.output_to_file.path={}".format(output_json),
"-E", "setup.template.fields={}".format(fields_yml)])
assert exit_code == 1

# check json file
with open(output_json) as f:
tmpl = json.load(f)
assert "mappings" in tmpl

def test_generate_templates_v5(self):
"""
Generates templates from other Beats.
"""
self.render_config_template()

output_json = os.path.join(self.working_dir, "template-5x.json")
fields_yml = "../../../../fields.yml"

exit_code = self.run_beat(extra_args=[
"-E", "setup.template.output_to_file.path={}".format(output_json),
"-E", "setup.template.output_to_file.version=5.0.0".format(output_json),
"-E", "setup.template.fields={}".format(fields_yml)])
assert exit_code == 1

# check json file
with open(output_json) as f:
tmpl = json.load(f)
assert "mappings" in tmpl
assert tmpl["mappings"]["_default_"]["_all"]["norms"]["enabled"] is False