From 4dce54b238e5edf53f2f6a7c3f0cc85540dbf2b7 Mon Sep 17 00:00:00 2001 From: Nicolas Ruflin Date: Mon, 18 Mar 2019 14:04:59 +0100 Subject: [PATCH] [Metricbeat] Check that fields are documented in data tests (#11127) Currently we check in python tests that the fields are documented. As by now we have all the fields also available in the go code with the fields.go files it is possible to do this check in the new data tests. To prevent cyclic imports the data_test.go had to be moved to its own package. Now by default all modules are imported. Like this we don't have to add each module manually. --- metricbeat/docs/fields.asciidoc | 2 +- metricbeat/mb/testing/{ => data}/data_test.go | 79 +++++++++++++------ metricbeat/mb/testing/modules.go | 18 ++--- .../kubernetes/apiserver/_meta/fields.yml | 4 +- metricbeat/module/kubernetes/fields.go | 2 +- 5 files changed, 70 insertions(+), 35 deletions(-) rename metricbeat/mb/testing/{ => data}/data_test.go (74%) diff --git a/metricbeat/docs/fields.asciidoc b/metricbeat/docs/fields.asciidoc index e3adf042f6d2..c62d528e6240 100644 --- a/metricbeat/docs/fields.asciidoc +++ b/metricbeat/docs/fields.asciidoc @@ -12276,7 +12276,7 @@ Request latency, number of requests -- -*`kubernetes.apiserver.request.latency.bucket`*:: +*`kubernetes.apiserver.request.latency.bucket.*`*:: + -- type: object diff --git a/metricbeat/mb/testing/data_test.go b/metricbeat/mb/testing/data/data_test.go similarity index 74% rename from metricbeat/mb/testing/data_test.go rename to metricbeat/mb/testing/data/data_test.go index 37c2b1eb7ac1..b33dd9de789d 100644 --- a/metricbeat/mb/testing/data_test.go +++ b/metricbeat/mb/testing/data/data_test.go @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -package testing +package data import ( "encoding/json" @@ -31,23 +31,17 @@ import ( "testing" "github.com/mitchellh/hashstructure" - "github.com/stretchr/testify/assert" "gopkg.in/yaml.v2" + "github.com/elastic/beats/libbeat/asset" "github.com/elastic/beats/libbeat/common" + "github.com/elastic/beats/libbeat/mapping" "github.com/elastic/beats/metricbeat/mb" + mbtesting "github.com/elastic/beats/metricbeat/mb/testing" - // TODO: generate include file for these tests automatically moving forward - _ "github.com/elastic/beats/metricbeat/module/couchbase/cluster" - _ "github.com/elastic/beats/metricbeat/module/couchbase/node" - _ "github.com/elastic/beats/metricbeat/module/kibana/status" - _ "github.com/elastic/beats/metricbeat/module/kubernetes/apiserver" - _ "github.com/elastic/beats/metricbeat/module/kubernetes/state_node" - _ "github.com/elastic/beats/metricbeat/module/php_fpm/pool" - _ "github.com/elastic/beats/metricbeat/module/php_fpm/process" - _ "github.com/elastic/beats/metricbeat/module/rabbitmq/connection" - _ "github.com/elastic/beats/metricbeat/module/traefik/health" + _ "github.com/elastic/beats/metricbeat/include" + _ "github.com/elastic/beats/metricbeat/include/fields" ) const ( @@ -72,8 +66,8 @@ func TestAll(t *testing.T) { for _, f := range configFiles { // get module and metricset name from path s := strings.Split(f, string(os.PathSeparator)) - moduleName := s[3] - metricSetName := s[4] + moduleName := s[4] + metricSetName := s[5] configFile, err := ioutil.ReadFile(f) if err != nil { @@ -95,7 +89,11 @@ func TestAll(t *testing.T) { func getTestdataFiles(t *testing.T, url, module, metricSet, suffix string) { - ff, _ := filepath.Glob(getMetricsetPath(module, metricSet) + "/_meta/testdata/*." + suffix) + ff, err := filepath.Glob(getMetricsetPath(module, metricSet) + "/_meta/testdata/*." + suffix) + if err != nil { + t.Fatal(err) + } + var files []string for _, f := range ff { // Exclude all the expected files @@ -118,18 +116,18 @@ func runTest(t *testing.T, file string, module, metricSetName, url, suffix strin s := server(t, file, url) defer s.Close() - metricSet := newMetricSet(t, getConfig(module, metricSetName, s.URL)) + metricSet := mbtesting.NewMetricSet(t, getConfig(module, metricSetName, s.URL)) var events []mb.Event var errs []error switch v := metricSet.(type) { case mb.ReportingMetricSetV2: - metricSet := NewReportingMetricSetV2(t, getConfig(module, metricSetName, s.URL)) - events, errs = ReportingFetchV2(metricSet) + metricSet := mbtesting.NewReportingMetricSetV2(t, getConfig(module, metricSetName, s.URL)) + events, errs = mbtesting.ReportingFetchV2(metricSet) case mb.ReportingMetricSetV2Error: - metricSet := NewReportingMetricSetV2Error(t, getConfig(module, metricSetName, s.URL)) - events, errs = ReportingFetchV2Error(metricSet) + metricSet := mbtesting.NewReportingMetricSetV2Error(t, getConfig(module, metricSetName, s.URL)) + events, errs = mbtesting.ReportingFetchV2Error(metricSet) default: t.Fatalf("unknown type: %T", v) } @@ -143,7 +141,7 @@ func runTest(t *testing.T, file string, module, metricSetName, url, suffix strin var data []common.MapStr for _, e := range events { - beatEvent := StandardizeEvent(metricSet, e, mb.AddMetricSetInfo) + beatEvent := mbtesting.StandardizeEvent(metricSet, e, mb.AddMetricSetInfo) // Overwrite service.address as the port changes every time beatEvent.Fields.Put("service.address", "127.0.0.1:55555") data = append(data, beatEvent.Fields) @@ -156,6 +154,8 @@ func runTest(t *testing.T, file string, module, metricSetName, url, suffix strin return h1 < h2 }) + checkDocumented(t, data) + output, err := json.MarshalIndent(&data, "", " ") if err != nil { t.Fatal(err) @@ -190,6 +190,41 @@ func writeDataJSON(t *testing.T, data common.MapStr, module, metricSet string) { } } +// checkDocumented checks that all fields which show up in the events are documented +func checkDocumented(t *testing.T, data []common.MapStr) { + fieldsData, err := asset.GetFields("metricbeat") + if err != nil { + t.Fatal(err) + } + + fields, err := mapping.LoadFields(fieldsData) + if err != nil { + t.Fatal(err) + } + documentedFields := fields.GetKeys() + keys := map[string]interface{}{} + + for _, k := range documentedFields { + keys[k] = struct{}{} + } + + for _, d := range data { + flat := d.Flatten() + for k := range flat { + if _, ok := keys[k]; !ok { + // If a field is defined as object it can also be defined as `status_codes.*` + // So this checks if such a key with the * exists by removing the last part. + splits := strings.Split(k, ".") + prefix := strings.Join(splits[0:len(splits)-1], ".") + if _, ok := keys[prefix+".*"]; ok { + continue + } + t.Fatalf("key missing: %s", k) + } + } + } +} + // GetConfig returns config for elasticsearch module func getConfig(module, metricSet, url string) map[string]interface{} { return map[string]interface{}{ @@ -226,7 +261,7 @@ func server(t *testing.T, path string, url string) *httptest.Server { } func getModulesPath() string { - return "../../module" + return "../../../module" } func getModulePath(module string) string { diff --git a/metricbeat/mb/testing/modules.go b/metricbeat/mb/testing/modules.go index 234feee35687..ec5ebc0f5322 100644 --- a/metricbeat/mb/testing/modules.go +++ b/metricbeat/mb/testing/modules.go @@ -81,10 +81,10 @@ func NewTestModule(t testing.TB, config interface{}) *TestModule { return &TestModule{RawConfig: c} } -// newMetricSet instantiates a new MetricSet using the given configuration. +// NewMetricSet instantiates a new MetricSet using the given configuration. // The ModuleFactory and MetricSetFactory are obtained from the global // Registry. -func newMetricSet(t testing.TB, config interface{}) mb.MetricSet { +func NewMetricSet(t testing.TB, config interface{}) mb.MetricSet { c, err := common.NewConfigFrom(config) if err != nil { t.Fatal(err) @@ -112,7 +112,7 @@ func newMetricSet(t testing.TB, config interface{}) mb.MetricSet { // configuration. The ModuleFactory and MetricSetFactory are obtained from the // global Registry. func NewEventFetcher(t testing.TB, config interface{}) mb.EventFetcher { - metricSet := newMetricSet(t, config) + metricSet := NewMetricSet(t, config) fetcher, ok := metricSet.(mb.EventFetcher) if !ok { @@ -126,7 +126,7 @@ func NewEventFetcher(t testing.TB, config interface{}) mb.EventFetcher { // configuration. The ModuleFactory and MetricSetFactory are obtained from the // global Registry. func NewEventsFetcher(t testing.TB, config interface{}) mb.EventsFetcher { - metricSet := newMetricSet(t, config) + metricSet := NewMetricSet(t, config) fetcher, ok := metricSet.(mb.EventsFetcher) if !ok { @@ -137,7 +137,7 @@ func NewEventsFetcher(t testing.TB, config interface{}) mb.EventsFetcher { } func NewReportingMetricSet(t testing.TB, config interface{}) mb.ReportingMetricSet { - metricSet := newMetricSet(t, config) + metricSet := NewMetricSet(t, config) reportingMetricSet, ok := metricSet.(mb.ReportingMetricSet) if !ok { @@ -158,7 +158,7 @@ func ReportingFetch(metricSet mb.ReportingMetricSet) ([]common.MapStr, []error) // NewReportingMetricSetV2 returns a new ReportingMetricSetV2 instance. Then // you can use ReportingFetchV2 to perform a Fetch operation with the MetricSet. func NewReportingMetricSetV2(t testing.TB, config interface{}) mb.ReportingMetricSetV2 { - metricSet := newMetricSet(t, config) + metricSet := NewMetricSet(t, config) reportingMetricSetV2, ok := metricSet.(mb.ReportingMetricSetV2) if !ok { @@ -171,7 +171,7 @@ func NewReportingMetricSetV2(t testing.TB, config interface{}) mb.ReportingMetri // NewReportingMetricSetV2Error returns a new ReportingMetricSetV2 instance. Then // you can use ReportingFetchV2 to perform a Fetch operation with the MetricSet. func NewReportingMetricSetV2Error(t testing.TB, config interface{}) mb.ReportingMetricSetV2Error { - metricSet := newMetricSet(t, config) + metricSet := NewMetricSet(t, config) reportingMetricSetV2Error, ok := metricSet.(mb.ReportingMetricSetV2Error) if !ok { @@ -232,7 +232,7 @@ func ReportingFetchV2Error(metricSet mb.ReportingMetricSetV2Error) ([]mb.Event, // configuration. The ModuleFactory and MetricSetFactory are obtained from the // global Registry. func NewPushMetricSet(t testing.TB, config interface{}) mb.PushMetricSet { - metricSet := newMetricSet(t, config) + metricSet := NewMetricSet(t, config) pushMetricSet, ok := metricSet.(mb.PushMetricSet) if !ok { @@ -297,7 +297,7 @@ func RunPushMetricSet(duration time.Duration, metricSet mb.PushMetricSet) ([]com // configuration. The ModuleFactory and MetricSetFactory are obtained from the // global Registry. func NewPushMetricSetV2(t testing.TB, config interface{}) mb.PushMetricSetV2 { - metricSet := newMetricSet(t, config) + metricSet := NewMetricSet(t, config) pushMetricSet, ok := metricSet.(mb.PushMetricSetV2) if !ok { diff --git a/metricbeat/module/kubernetes/apiserver/_meta/fields.yml b/metricbeat/module/kubernetes/apiserver/_meta/fields.yml index c1fb4b055d39..1947cf981d4b 100644 --- a/metricbeat/module/kubernetes/apiserver/_meta/fields.yml +++ b/metricbeat/module/kubernetes/apiserver/_meta/fields.yml @@ -36,8 +36,8 @@ type: long description: > Request latency, number of requests - - name: request.latency.bucket + - name: request.latency.bucket.* type: object object_type: long description: > - Request latency histogram buckets \ No newline at end of file + Request latency histogram buckets diff --git a/metricbeat/module/kubernetes/fields.go b/metricbeat/module/kubernetes/fields.go index 63e6cb4a6618..7410273e3907 100644 --- a/metricbeat/module/kubernetes/fields.go +++ b/metricbeat/module/kubernetes/fields.go @@ -32,5 +32,5 @@ func init() { // AssetKubernetes returns asset data. // This is the base64 encoded gzipped contents of ../metricbeat/module/kubernetes. func AssetKubernetes() string { - return "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" + return "eJzsXU9v6zYSv79PMcgpb5HmtNhDDgt00y02eH8a5OW1h8XCoKWxzUYiVZJK6v30BSmJkiVSkm3acWLxlEj2zI8zw+GQM6R/gCdc38BTPkfBUKH8AKCoSvAGLj7ZhxcfAGKUkaCZopzdwD8/AADUH4AUlaCR/rbABInEG1iSDwASlaJsKW/gvxdSJhdXcLFSKrv4n3634kLNIs4WdHkDC5JI/ACwoJjE8sYw+AEYSbEFTze1zjQHwfOsfOKAp9sdW3CREv0YCItBKqKoVDSSwBeQ8VhCShhZYgzzdYPPdUmhiaaJiGRUonhGYd+4QPUAa8nvx/s7KAg2RFm1TZFWrQ2tCU/gHzlKdR0lFJna+EiF8wnXL1zErXc9aHW7NfQg5pQtQa2wYiR7UQiUPBcRhsPxUFDGGJy02wBkPj8kBh/5DoyIZ+EBgCELl1GSS4XiyjCVGYnwykrnYy+uZxTz8LD+8/h4Dx3SHQvlucdAE86W23F+5IokwPJ0jkIP71HGmRCFLFpfyzwNBKMUgISS9BXIPNV4iv8pSqAMUhoJLjHiLB4HMKSkKh1ZhDsKbZ5HT6iu/+aExee/Y9RGXDycBQIOKyoVXwqSQgFFdjx1xJkilO3nqeuJoaYXxFFLRYSaKZq6/UJMVPvFgIC+aYLQIWilkeVORm1ZjOB0e/8dckmW6BCEr9tNKOa7nbd9gPqobnSSCxfhYeJDDJpMWLu/XTYO8262Afk22601Oi31Wy6wFD0jzOlEOmgJ41osPtCDgEeCLYwC4wGGFhaP8TrrOIlNVDIiCcazRcKJ74NFkHcDGYoImXIb1tbd0AImEkiDrPaQOu5RxVTDYwSSJDwiiswT1N/r7W9CU6reZIdjXFCGcdEDzd48rZ3hpX7iFQrQBeTMfBdjdzCS8GXbVnZ2TZ/5Us+xC76lSyLPhCYa80Hc0nytdh9/lcL7iIzUtZGO7SpEJCMRVWsdlLipW79afvL9S6ew5PGS0S7v/UvFOPbxQqHaE7j4hpjhu7HwJvX9p7JiNVGPE293algLgf2BRyhUmtEYQB67DA/ImIYDUAUkxZSLtuPw28HkqMFhgU4hHiOgPi2BFGLwdvf0o8svjQ5sGWB6TADeQow5ptt7hJmlWfgjzaaQhDzMxHQqI+Xh27f+cVIBfuHiibKlRJcVvB95/FZ0EySqcXLJyBIXJE+U3048yEcg+mp32zQb8PCxcyf5nYsj4TG8vKjs6OFcLcav1oZm87NYVzxwrmBBE5RrqTDdeolxHiGPW0rNIPzcV2JuCZXx9+utyI6w0vjuWGNU7PF5M8+59Q7/4wqb+VhDz6a1UUHEkwQjZd+oFVFABMISGQqiigRykd2QIHLGaKu/lEkam0DnUzudDVvkDvxLYI+ce6V7q6kUXEBgxEUsTcRV54MUTbF4lhGhaJQnRBRCgBWRwKMoF2JD9xVC801F0syBsmtqfVmSBRVSzUpWzJPE3T5X8lgB1P00PKDmoZ+1raoRZJODA9IsBvDUq2vZCWb8GdxeEF8KUqUxYGxj8CV9RuaQSMSz9UxxF4g6a0gkZyHQPRhKY8FZQ1xnQWTzuM7sQqWfY4qKxESRUZY/oI+CEhApeUSNo3mhatWrk76x5B6V24dv1g8J1KA6tuwdAyP8/cY4MAwoZ/2Sb6a6PMnUnaoZvpLU6ryfpym5CMvYkNTT+suKRqvS674QWU867vC8rPqYPaOQtDXy9gL1a0FwQyD9JTg5bbPYg/13Rv/IEWiMTNEFRQGKN4A4Cg5snh2TxSyh7CkgmIfPIDATKDWash7K5xAoe+bJM8YzB8ZD+YWKp0sufR6CZDS85fx4fwfPm9bTo64nygKajeatKY5gHNZ5sIbz6GF6uPFaUd5C9GEH7Pe7nwZ4Nzdr9wngGyU6Zmtwqs6ZqnM8LXR1zldtb2+7MGfK07nalKdrtXB5uikR0wI8JWLcwKdETE8ihqHSdhPMX4s/37XxPWCE9Nls1fpo2Q1lIbg49KT88KePj92ted8KeRSEyZQqdTo6eXTqxO5ET1nPoo2U5s9TwnNLAU25zrp1hHMOac46BvAVVbZBHaMatkZ1GnWwNR5fLayNaXLm3cHZxW/TVEeAB6pr9s8JwwyGmMDIEQ5jt0jGjHTYbivlLjUR7/azBoycOeCcxThiboFtnN0ZitA9A9nFKm8KbJ897IzHb3ILe1qRFm1akdbtLSnkza1IzyJndCJZkg6sEz1gss3x5XM7sqwnVnueRLYPlIw7qxw4SzYlhFqwT3VcTQe3gg62nU9vncfW4Mag8Xe5lUCcvfcMYiGWl04e0b+EeOcp5kIgAqWpizQSkfT/g2UIGVni7GCZzALU6Kzq7Bho/DnVeif1lC+L2rkyrxvpW1zUXUIsFVF5uFxXtiLS76zdHWh3oi8UtN0xjOCyPH9zBS+EKvOHQpFSRvoPBSOJ/em4OecJknZx5kiUNULDxC3fjYpQRUTPUKBM4XLDTHcEU/DxpBB6DnA0weylv98KDcGlRXVrCv610m4FkavPnGf/ItETXyyu4N9CmI25+zxJrsD+Wb7vqlY3HXSU2qecaUZplqDC+KqWxC1hjKuHnBkWXFzBL798+USTBOOPZfev915+D42SIgD0LTv39YGbd4gZLgXHHrVXdzQeA5Gw92m6+W1KqW+BPoArExhpR3AD/7j+ewjkFstIefZhH4Z3KKkftTC0UKIv+Ont4lDktJUIyui6WLUMJpYrBb4+7lpt1cLJl5KIMUv4Ot3zeGwjpqkJBglqMuJIM/XNuANy+uREWnBxTbu1brOERmR8zLMTjorLLrfTxSip6Kn42Csm+KnGWJ/9LTnWqC9lhtE+6/JQGOtMpkdvjd01djxYDV4jgGWx8+RkcFAFny6gEz96E3IB0h/a7xXAmtMfzageLpXI8aq40V5Hnzl7YvyF+cdNzmS0wjjvN9K9FiAG5QafPmcYMqpt7PINRJK+Pc2x3dPxTHNPsT+OrGpKDhbeWky2euV4524aMn+tYOWrb4t37DWjr4u8RNtfeeSu+4BgujOb5YcyzaZuMu44p9ZRyEHhmFxBu/7tdAtrqPsChc7j8WGiRnZ372S24lLNDsNRk/ax3XIS3o5xOVnuVmpwwA3FFsxyR/Gh2lG8RxZTtry+vt51IzEkuv3ijjIa6IlBQ2K13Fx4r7po2yszDLWCLQmWuaMQv7ZzsKVjE6p/DRviIO4eyZWN+6LsWjFDAQ/FP98cKcmxq9rXwtU/hsOh0uN3W2x8bn4r6lBCK2+XMXc2lJxgvjYJ/RqcyW0JniSOFard5SNz7PMuoaS4yJNkXXEblGZzdsNFnoRzLBXFYJ7FfbmSV3hD14utsL5Nyd4DBZeY8Wj10RRtfCt70La+I7i6DeFZHe7k7Q48PmrDs8Njw+Z8QoRXcHudLbw+gBW42gEcWs8NV0Prn+o7LXVbJTfAnoaaK+WOAGadnjnMEMrfFScjGhVeQZyeowgDtqh2cFU228QJa1/w9FoHJc6ibvv93fUzXfPjIDcVMJ9RueF0o81mm2602fdGmwrNM0/yNFQmsiAWJCDpxAx7xSK/FsC8gch0xUjZpitGpitG3B+YrhjZr9OuX1JwQTnCPR4/j/wlu+P94l8J5q8AAAD//7mzpss=" }