From 8c1446261cf1528222ba6c2bbcdb129ebff75f49 Mon Sep 17 00:00:00 2001 From: Andrew Wilkins Date: Tue, 18 Aug 2020 14:06:16 +0800 Subject: [PATCH] systemtest: add tests for aggregation and sampling Migrate aggregation and sampling system tests to Go. As part of this we introduce "approval" test support. The implementation is a little different to the one in the Python system tests: the Python ones ignore various dynamic fields when computing the diff; we replace dynamic fields before comparing, so the approved diffs don't change in unrelated ways every time there is an expected change, such as a new field. --- systemtest/aggregation_test.go | 109 +++++++++++ systemtest/apmservertest/config.go | 32 +++- systemtest/apmservertest/filter.go | 116 ++++++++++++ systemtest/apmservertest/server.go | 28 ++- systemtest/approvals.go | 171 +++++++++++++++++ .../TestTransactionAggregation.approved.json | 109 +++++++++++ systemtest/estest/query.go | 10 + systemtest/go.mod | 3 + systemtest/go.sum | 10 +- systemtest/sampling_test.go | 73 +++++++ ...ransaction_histogram_metrics.approved.json | 50 ----- tests/system/test_aggregation.py | 69 ------- ...ransaction_histogram_metrics.approved.json | 179 ------------------ 13 files changed, 651 insertions(+), 308 deletions(-) create mode 100644 systemtest/aggregation_test.go create mode 100644 systemtest/apmservertest/filter.go create mode 100644 systemtest/approvals.go create mode 100644 systemtest/approvals/TestTransactionAggregation.approved.json create mode 100644 systemtest/sampling_test.go delete mode 100644 tests/system/rum_transaction_histogram_metrics.approved.json delete mode 100644 tests/system/test_aggregation.py delete mode 100644 tests/system/transaction_histogram_metrics.approved.json diff --git a/systemtest/aggregation_test.go b/systemtest/aggregation_test.go new file mode 100644 index 00000000000..d2d7d73a8ee --- /dev/null +++ b/systemtest/aggregation_test.go @@ -0,0 +1,109 @@ +// 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 systemtest_test + +import ( + "context" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/apm-server/systemtest" + "github.com/elastic/apm-server/systemtest/apmservertest" + "github.com/elastic/apm-server/systemtest/estest" +) + +func TestTransactionAggregation(t *testing.T) { + systemtest.CleanupElasticsearch(t) + srv := apmservertest.NewUnstartedServer(t) + srv.Config.Aggregation = &apmservertest.TransactionAggregationConfig{ + Enabled: true, + Interval: time.Second, + } + srv.Config.Sampling = &apmservertest.SamplingConfig{ + // Drop unsampled transaction events, to show + // that we aggregate before they are dropped. + KeepUnsampled: false, + } + err := srv.Start() + require.NoError(t, err) + + // Send some transactions to the server to be aggregated. + // + // Mimic a RUM transaction by using the "page-load" transaction type, + // which causes user-agent to be parsed and included in the aggregation + // and added to the document fields. + tracer := srv.Tracer() + const chromeUserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36" + for _, transactionType := range []string{"backend", "page-load"} { + tx := tracer.StartTransaction("name", transactionType) + req, _ := http.NewRequest("GET", "/", nil) + req.Header.Set("User-Agent", chromeUserAgent) + tx.Context.SetHTTPRequest(req) + tx.Duration = time.Second + tx.End() + } + tracer.Flush(nil) + + var result estest.SearchResult + _, err = systemtest.Elasticsearch.Search("apm-*").WithQuery(estest.BoolQuery{ + Filter: []interface{}{ + estest.ExistsQuery{Field: "transaction.duration.histogram"}, + }, + }).Do(context.Background(), &result, + estest.WithCondition(result.Hits.NonEmptyCondition()), + ) + require.NoError(t, err) + systemtest.ApproveEvents(t, t.Name(), result.Hits.Hits) +} + +func TestTransactionAggregationShutdown(t *testing.T) { + systemtest.CleanupElasticsearch(t) + srv := apmservertest.NewUnstartedServer(t) + srv.Config.Aggregation = &apmservertest.TransactionAggregationConfig{ + Enabled: true, + // Set aggregation_interval to something that would cause + // a timeout if we were to wait that long. The server + // should flush metrics on shutdown without waiting for + // the configured interval. + Interval: time.Minute, + } + err := srv.Start() + require.NoError(t, err) + + // Send a transaction to the server to be aggregated. + tracer := srv.Tracer() + tracer.StartTransaction("name", "type").End() + tracer.Flush(nil) + + // Stop server to ensure metrics are flushed on shutdown. + assert.NoError(t, srv.Close()) + + var result estest.SearchResult + _, err = systemtest.Elasticsearch.Search("apm-*").WithQuery(estest.BoolQuery{ + Filter: []interface{}{ + estest.ExistsQuery{Field: "transaction.duration.histogram"}, + }, + }).Do(context.Background(), &result, + estest.WithCondition(result.Hits.NonEmptyCondition()), + ) + require.NoError(t, err) +} diff --git a/systemtest/apmservertest/config.go b/systemtest/apmservertest/config.go index df38e3f697a..b0989e64935 100644 --- a/systemtest/apmservertest/config.go +++ b/systemtest/apmservertest/config.go @@ -41,9 +41,11 @@ const ( // Config holds APM Server configuration. type Config struct { - SecretToken string `json:"apm-server.secret_token,omitempty"` - Jaeger *JaegerConfig `json:"apm-server.jaeger,omitempty"` - Kibana *KibanaConfig `json:"apm-server.kibana,omitempty"` + SecretToken string `json:"apm-server.secret_token,omitempty"` + Jaeger *JaegerConfig `json:"apm-server.jaeger,omitempty"` + Kibana *KibanaConfig `json:"apm-server.kibana,omitempty"` + Aggregation *TransactionAggregationConfig `json:"apm-server.aggregation,omitempty"` + Sampling *SamplingConfig `json:"apm-server.sampling,omitempty"` // Instrumentation holds configuration for libbeat and apm-server instrumentation. Instrumentation *InstrumentationConfig `json:"instrumentation,omitempty"` @@ -99,6 +101,11 @@ type JaegerConfig struct { HTTPHost string `json:"http.host,omitempty"` } +// SamplingConfig holds APM Server trace sampling configuration. +type SamplingConfig struct { + KeepUnsampled bool `json:"keep_unsampled"` +} + // InstrumentationConfig holds APM Server instrumentation configuration. type InstrumentationConfig struct { Enabled bool `json:"enabled"` @@ -180,6 +187,25 @@ func (m *MonitoringConfig) MarshalJSON() ([]byte, error) { }) } +// TransactionAggregationConfig holds APM Server transaction metrics aggregation configuration. +type TransactionAggregationConfig struct { + Enabled bool + Interval time.Duration +} + +func (m *TransactionAggregationConfig) MarshalJSON() ([]byte, error) { + // time.Duration is encoded as int64. + // Convert time.Durations to durations, to encode as duration strings. + type config struct { + Enabled bool `json:"enabled"` + Interval duration `json:"interval,omitempty"` + } + return json.Marshal(config{ + Enabled: m.Enabled, + Interval: duration(m.Interval), + }) +} + type duration time.Duration func (d duration) MarshalText() (text []byte, err error) { diff --git a/systemtest/apmservertest/filter.go b/systemtest/apmservertest/filter.go new file mode 100644 index 00000000000..079976cf627 --- /dev/null +++ b/systemtest/apmservertest/filter.go @@ -0,0 +1,116 @@ +// 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 apmservertest + +import ( + "bytes" + "compress/zlib" + "context" + "encoding/json" + "io" + + "go.elastic.co/apm/model" + "go.elastic.co/apm/transport" + "go.elastic.co/fastjson" +) + +// TODO(axw) move EventMetadata and filteringTransport to go.elastic.co/apmtest, +// generalising filteringTransport to work with arbitrary base transports. To do +// that we would need to dynamically check for optional interfaces supported by +// the base transport, and create passthrough methods. + +// EventMetadata holds event metadata. +type EventMetadata struct { + System *model.System + Process *model.Process + Service *model.Service + Labels model.IfaceMap +} + +// EventMetadata holds event metadata. +type EventMetadataFilter interface { + FilterEventMetadata(*EventMetadata) +} + +type filteringTransport struct { + *transport.HTTPTransport + filter EventMetadataFilter +} + +// SendStream decodes metadata from reader, passes it through the filters, +// and then sends the modified stream to the underlying transport. +func (t *filteringTransport) SendStream(ctx context.Context, stream io.Reader) error { + zr, err := zlib.NewReader(stream) + if err != nil { + return err + } + decoder := json.NewDecoder(zr) + + // The first object of any request must be a metadata struct. + var metadataPayload struct { + Metadata EventMetadata `json:"metadata"` + } + if err := decoder.Decode(&metadataPayload); err != nil { + return err + } + t.filter.FilterEventMetadata(&metadataPayload.Metadata) + + // Re-encode metadata. + var json fastjson.Writer + json.RawString(`{"metadata":`) + json.RawString(`{"system":`) + metadataPayload.Metadata.System.MarshalFastJSON(&json) + json.RawString(`,"process":`) + metadataPayload.Metadata.Process.MarshalFastJSON(&json) + json.RawString(`,"service":`) + metadataPayload.Metadata.Service.MarshalFastJSON(&json) + if len(metadataPayload.Metadata.Labels) > 0 { + json.RawString(`,"labels":`) + metadataPayload.Metadata.Labels.MarshalFastJSON(&json) + } + json.RawString("}}\n") + + // Copy everything to a new zlib-encoded stream and send. + var buf bytes.Buffer + zw := zlib.NewWriter(&buf) + zw.Write(json.Bytes()) + if _, err := io.Copy(zw, io.MultiReader(decoder.Buffered(), zr)); err != nil { + return err + } + if err := zw.Close(); err != nil { + return err + } + return t.HTTPTransport.SendStream(ctx, &buf) +} + +type defaultMetadataFilter struct{} + +func (defaultMetadataFilter) FilterEventMetadata(m *EventMetadata) { + m.System.Platform = "minix" + m.System.Architecture = "i386" + m.System.Container = nil + m.System.Kubernetes = nil + m.System.Hostname = "beowulf" + m.Process.Pid = 1 + m.Process.Ppid = nil + m.Service.Agent.Version = "0.0.0" + m.Service.Language.Version = "2.0" + m.Service.Runtime.Version = "2.0" + m.Service.Node = nil + m.Service.Name = "systemtest" +} diff --git a/systemtest/apmservertest/server.go b/systemtest/apmservertest/server.go index cee4bc02334..bfbc3dfc4b4 100644 --- a/systemtest/apmservertest/server.go +++ b/systemtest/apmservertest/server.go @@ -77,6 +77,15 @@ type Server struct { // JaegerHTTPURL holds the base URL for Jaeger HTTP, if enabled. JaegerHTTPURL string + // EventMetadataFilter holds an optional EventMetadataFilter, which + // can modify event metadata before it is sent to the server. + // + // New(Unstarted)Server sets a default filter which removes or + // replaces environment-specific properties such as host name, + // container ID, etc., to enable repeatable tests across different + // test environments. + EventMetadataFilter EventMetadataFilter + tb testing.TB args []string cmd *ServerCmd @@ -96,9 +105,10 @@ func NewServer(tb testing.TB, args ...string) *Server { // apm-server command. func NewUnstartedServer(tb testing.TB, args ...string) *Server { return &Server{ - Config: DefaultConfig(), - tb: tb, - args: args, + Config: DefaultConfig(), + EventMetadataFilter: defaultMetadataFilter{}, + tb: tb, + args: args, } } @@ -371,9 +381,15 @@ func (s *Server) Tracer() *apm.Tracer { } httpTransport.SetServerURL(serverURL) httpTransport.SetSecretToken(s.Config.SecretToken) - tracer, err := apm.NewTracerOptions(apm.TracerOptions{ - Transport: httpTransport, - }) + + var transport transport.Transport = httpTransport + if s.EventMetadataFilter != nil { + transport = &filteringTransport{ + HTTPTransport: httpTransport, + filter: s.EventMetadataFilter, + } + } + tracer, err := apm.NewTracerOptions(apm.TracerOptions{Transport: transport}) if err != nil { s.tb.Fatal(err) } diff --git a/systemtest/approvals.go b/systemtest/approvals.go new file mode 100644 index 00000000000..d9a5ddaba2e --- /dev/null +++ b/systemtest/approvals.go @@ -0,0 +1,171 @@ +// 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 systemtest + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/elastic/apm-server/systemtest/estest" +) + +const ( + // approvedSuffix signals a file has been reviewed and approved + approvedSuffix = ".approved.json" + + // receivedSuffix signals a file has changed and not yet been approved + receivedSuffix = ".received.json" +) + +// ApproveEvents compares the _source of the search hits with +// the contents of the file in "systemtest/approvals/". +// +// Dynamic fields (@timestamp, observer.id, etc.) are replaced +// with a static string for comparison. Integration tests elsewhere +// use canned data to test fields that we do not cover here. +// +// If the events differ, then the test will fail. +func ApproveEvents(t testing.TB, name string, hits []estest.SearchHit) { + // Fields generated by the server (e.g. observer.*) or + // agent (e.g. transaction.id) which may change between + // tests. + // + // Ignore their values in comparisons, but compare + // existence: either the field exists in both, or neither. + dynamicFields := []string{ + "@timestamp", + "ecs.version", + "event.ingested", + "observer.ephemeral_id", + "observer.hostname", + "observer.id", + "observer.version", + "observer.version_major", + } + + // Sort events for repeatable diffs. + sort.Sort(apmEventSearchHits(hits)) + + // Finally, rewrite all other dynamic fields. + var events []interface{} + for _, hit := range hits { + source := []byte(hit.RawSource) + for _, field := range dynamicFields { + existing := gjson.GetBytes(source, field) + if !existing.Exists() { + continue + } + + var err error + source, err = sjson.SetBytes(source, field, "dynamic") + if err != nil { + t.Fatal(err) + } + } + + var event map[string]interface{} + if err := json.Unmarshal(source, &event); err != nil { + t.Fatal(err) + } + events = append(events, event) + } + + var approved []interface{} + readApproved(name, &approved) + if diff := cmp.Diff(approved, events); diff != "" { + writeReceived(name, events) + t.Fatalf("%s\n%s\n\n", diff, + "Run `make update check-approvals` to verify the diff", + ) + } else { + // Remove an old *.received.json file if it exists. + removeReceived(name) + } +} + +func readApproved(name string, approved interface{}) { + path := filepath.Join("approvals", name+approvedSuffix) + f, err := os.Open(path) + if os.IsNotExist(err) { + return + } else if err != nil { + panic(err) + } + defer f.Close() + if err := json.NewDecoder(f).Decode(&approved); err != nil { + panic(fmt.Errorf("cannot unmarshal file %q: %w", path, err)) + } +} + +func removeReceived(name string) { + path := filepath.Join("approvals", name+receivedSuffix) + os.Remove(path) +} + +func writeReceived(name string, received interface{}) { + path := filepath.Join("approvals", name+receivedSuffix) + f, err := os.Create(path) + if err != nil { + panic(err) + } + defer f.Close() + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + if err := enc.Encode(received); err != nil { + panic(err) + } +} + +var apmEventSortFields = []string{ + "processor.event", + "trace.id", + "transaction.id", + "span.id", + "error.id", + "timeseries.instance", + "@timestamp", // last resort, order is generally guaranteed +} + +type apmEventSearchHits []estest.SearchHit + +func (hits apmEventSearchHits) Len() int { + return len(hits) +} + +func (hits apmEventSearchHits) Swap(i, j int) { + hits[i], hits[j] = hits[j], hits[i] +} + +func (hits apmEventSearchHits) Less(i, j int) bool { + for _, field := range apmEventSortFields { + ri := gjson.GetBytes(hits[i].RawSource, field) + rj := gjson.GetBytes(hits[j].RawSource, field) + if ri.Less(rj, true) { + return true + } + } + return false +} diff --git a/systemtest/approvals/TestTransactionAggregation.approved.json b/systemtest/approvals/TestTransactionAggregation.approved.json new file mode 100644 index 00000000000..0b3203362c4 --- /dev/null +++ b/systemtest/approvals/TestTransactionAggregation.approved.json @@ -0,0 +1,109 @@ +[ + { + "@timestamp": "dynamic", + "agent": { + "name": "go" + }, + "ecs": { + "version": "dynamic" + }, + "event": { + "ingested": "dynamic" + }, + "host": { + "hostname": "beowulf", + "name": "beowulf" + }, + "observer": { + "ephemeral_id": "dynamic", + "hostname": "dynamic", + "id": "dynamic", + "type": "apm-server", + "version": "dynamic", + "version_major": "dynamic" + }, + "processor": { + "event": "metric", + "name": "metric" + }, + "service": { + "name": "systemtest", + "node": { + "name": "beowulf" + } + }, + "timeseries": { + "instance": "systemtest:name:7363045c23d03444" + }, + "transaction": { + "duration": { + "histogram": { + "counts": [ + 1 + ], + "values": [ + 1003519 + ] + } + }, + "name": "name", + "root": true, + "type": "backend" + } + }, + { + "@timestamp": "dynamic", + "agent": { + "name": "go" + }, + "ecs": { + "version": "dynamic" + }, + "event": { + "ingested": "dynamic" + }, + "host": { + "hostname": "beowulf", + "name": "beowulf" + }, + "observer": { + "ephemeral_id": "dynamic", + "hostname": "dynamic", + "id": "dynamic", + "type": "apm-server", + "version": "dynamic", + "version_major": "dynamic" + }, + "processor": { + "event": "metric", + "name": "metric" + }, + "service": { + "name": "systemtest", + "node": { + "name": "beowulf" + } + }, + "timeseries": { + "instance": "systemtest:name:d1fefeee84e87666" + }, + "transaction": { + "duration": { + "histogram": { + "counts": [ + 1 + ], + "values": [ + 1003519 + ] + } + }, + "name": "name", + "root": true, + "type": "page-load" + }, + "user_agent": { + "name": "Chrome" + } + } +] diff --git a/systemtest/estest/query.go b/systemtest/estest/query.go index fc3c45dcdcb..db24fb19631 100644 --- a/systemtest/estest/query.go +++ b/systemtest/estest/query.go @@ -40,6 +40,16 @@ func (q BoolQuery) MarshalJSON() ([]byte, error) { return encodeQueryJSON("bool", boolQuery(q)) } +type ExistsQuery struct { + Field string +} + +func (q ExistsQuery) MarshalJSON() ([]byte, error) { + return encodeQueryJSON("exists", map[string]interface{}{ + "field": q.Field, + }) +} + type TermQuery struct { Field string Value interface{} diff --git a/systemtest/go.mod b/systemtest/go.mod index 378c5cdbc6d..2aec0030f26 100644 --- a/systemtest/go.mod +++ b/systemtest/go.mod @@ -10,12 +10,15 @@ require ( github.com/docker/docker v1.13.1 github.com/docker/go-connections v0.4.0 // indirect github.com/elastic/go-elasticsearch/v7 v7.8.0 + github.com/google/go-cmp v0.4.0 github.com/jaegertracing/jaeger v1.18.1 github.com/mitchellh/mapstructure v1.1.2 github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/stretchr/testify v1.6.1 + github.com/tidwall/gjson v1.6.0 + github.com/tidwall/sjson v1.1.1 go.elastic.co/apm v1.8.0 go.elastic.co/fastjson v1.0.0 go.uber.org/zap v1.15.0 diff --git a/systemtest/go.sum b/systemtest/go.sum index 120cd470eef..b2b4257be72 100644 --- a/systemtest/go.sum +++ b/systemtest/go.sum @@ -70,7 +70,6 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dop251/goja_nodejs v0.0.0-20200811150831-9bc458b4bbeb h1:UGtCiVzBK40WGYBmNui17MHCkAqdo1j3BbhtU3mB1fI= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= @@ -377,7 +376,15 @@ github.com/stretchr/testify v1.5.0/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc= +github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= +github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.0.1 h1:WE4RBSZ1x6McVVC8S/Md+Qse8YUv6HRObAx6ke00NY8= +github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/sjson v1.1.1 h1:7h1vk049Jnd5EH9NyzNiEuwYW4b5qgreBbqRC19AS3U= +github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/uber/jaeger-client-go v2.22.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= @@ -565,6 +572,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/systemtest/sampling_test.go b/systemtest/sampling_test.go new file mode 100644 index 00000000000..98c10bdf5f2 --- /dev/null +++ b/systemtest/sampling_test.go @@ -0,0 +1,73 @@ +// 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 systemtest_test + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.elastic.co/apm" + + "github.com/elastic/apm-server/systemtest" + "github.com/elastic/apm-server/systemtest/apmservertest" + "github.com/elastic/apm-server/systemtest/estest" +) + +func TestKeepUnsampled(t *testing.T) { + for _, keepUnsampled := range []bool{false, true} { + t.Run(fmt.Sprint(keepUnsampled), func(t *testing.T) { + systemtest.CleanupElasticsearch(t) + srv := apmservertest.NewUnstartedServer(t) + srv.Config.Sampling = &apmservertest.SamplingConfig{ + KeepUnsampled: keepUnsampled, + } + err := srv.Start() + require.NoError(t, err) + + // Send one unsampled transaction, and one sampled transaction. + transactionType := "TestKeepUnsampled" + tracer := srv.Tracer() + tracer.StartTransaction("sampled", transactionType).End() + tracer.SetSampler(apm.NewRatioSampler(0)) + tracer.StartTransaction("unsampled", transactionType).End() + tracer.Flush(nil) + + var result estest.SearchResult + _, err = systemtest.Elasticsearch.Search("apm-*").WithQuery(estest.BoolQuery{ + Filter: []interface{}{ + estest.TermQuery{ + Field: "transaction.type", + Value: transactionType, + }, + }, + }).Do(context.Background(), &result, + estest.WithCondition(result.Hits.NonEmptyCondition()), + ) + require.NoError(t, err) + + expectedTransactionDocs := 1 + if keepUnsampled { + expectedTransactionDocs++ + } + assert.Len(t, result.Hits.Hits, expectedTransactionDocs) + }) + } +} diff --git a/tests/system/rum_transaction_histogram_metrics.approved.json b/tests/system/rum_transaction_histogram_metrics.approved.json deleted file mode 100644 index 5c839d8eade..00000000000 --- a/tests/system/rum_transaction_histogram_metrics.approved.json +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "@timestamp": "2020-04-14T08:56:03.100Z", - "agent": { - "name": "rum-js" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-15T03:33:39.684986Z" - }, - "observer": { - "ephemeral_id": "e9e37f05-448f-4afd-8373-af8b6ff2d6cc", - "hostname": "goat", - "id": "b0f48ba0-ef6f-46af-922d-6be593c437c2", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "processor": { - "event": "metric", - "name": "metric" - }, - "service": { - "name": "apm-agent-js", - "version": "1.0.0" - }, - "timeseries": { - "instance": "apm-agent-js::2ac4d2f17a65a386" - }, - "transaction": { - "duration": { - "histogram": { - "counts": [ - 1 - ], - "values": [ - 643071 - ] - } - }, - "root": true, - "type": "page-load" - }, - "user_agent": { - "name": "Other" - } - } -] \ No newline at end of file diff --git a/tests/system/test_aggregation.py b/tests/system/test_aggregation.py deleted file mode 100644 index 2a132cb5372..00000000000 --- a/tests/system/test_aggregation.py +++ /dev/null @@ -1,69 +0,0 @@ -import time - -from apmserver import integration_test -from apmserver import ClientSideElasticTest, ElasticTest, ExpvarBaseTest, ProcStartupFailureTest -from helper import wait_until -from es_helper import index_smap, index_metric, index_transaction - - -@integration_test -class Test(ElasticTest): - def config(self): - cfg = super(Test, self).config() - cfg.update({ - "aggregation_enabled": True, - "aggregation_interval": "1s", - # Drop unsampled transaction events, - # to show that we aggregate before they - # are dropped. - "sampling_keep_unsampled": False, - }) - return cfg - - def test_transaction_metrics(self): - self.load_docs_with_template(self.get_payload_path("transactions_spans.ndjson"), - self.intake_url, 'transaction', 8) - self.assert_no_logged_warnings() - - self.wait_for_events('transaction', 3, index=index_transaction) - - metric_docs = self.wait_for_events('metric', 3, index=index_metric) - for doc in metric_docs: - # @timestamp is dynamic, so set it to something known. - doc['_source']['@timestamp'] = '2020-04-14T08:56:03.100Z' - self.approve_docs('transaction_histogram_metrics', metric_docs) - - def test_rum_transaction_metrics(self): - self.load_docs_with_template(self.get_payload_path("transactions_spans_rum.ndjson"), - self.intake_url, 'transaction', 2) - self.assert_no_logged_warnings() - - self.wait_for_events('transaction', 1, index=index_transaction) - - metric_docs = self.wait_for_events('metric', 1, index=index_metric) - for doc in metric_docs: - # @timestamp is dynamic, so set it to something known. - doc['_source']['@timestamp'] = '2020-04-14T08:56:03.100Z' - self.approve_docs('rum_transaction_histogram_metrics', metric_docs) - - -@integration_test -class TestShutdown(ElasticTest): - def config(self): - cfg = super(TestShutdown, self).config() - cfg.update({ - "aggregation_enabled": True, - # Set aggregation_interval to something that would cause - # a timeout if we were to wait that long. The server - # should flush metrics on shutdown without waiting for - # the configured interval. - "aggregation_interval": "180s", - }) - return cfg - - def test_transaction_metrics_flushed_shutdown(self): - self.load_docs_with_template(self.get_payload_path("transactions_spans.ndjson"), - self.intake_url, 'transaction', 9) - self.assert_no_logged_warnings() - self.apmserver_proc.kill() # Stop server to ensure metrics are flushed on shutdown - self.wait_for_events('metric', 3, index=index_metric) diff --git a/tests/system/transaction_histogram_metrics.approved.json b/tests/system/transaction_histogram_metrics.approved.json deleted file mode 100644 index 71f2f81d875..00000000000 --- a/tests/system/transaction_histogram_metrics.approved.json +++ /dev/null @@ -1,179 +0,0 @@ -[ - { - "@timestamp": "2020-04-14T08:56:03.100Z", - "agent": { - "name": "elastic-node" - }, - "container": { - "id": "container-id" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-04-24T01:53:20.081678Z" - }, - "kubernetes": { - "pod": { - "name": "pod-name" - } - }, - "observer": { - "ephemeral_id": "fdbbb18c-f372-4b20-81c1-ab6596a0703a", - "hostname": "alloy", - "id": "fb7241d5-1c02-43dc-8561-1b8c5f95ad5c", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "processor": { - "event": "metric", - "name": "metric" - }, - "service": { - "environment": "staging", - "name": "1234_service-12a3", - "node": { - "name": "container-id" - }, - "version": "5.1.3" - }, - "timeseries": { - "instance": "1234_service-12a3:GET /api/types:95818d7bc9580d28" - }, - "transaction": { - "duration": { - "histogram": { - "counts": [ - 2 - ], - "values": [ - 14015 - ] - } - }, - "name": "GET /api/types", - "result": "200", - "root": true, - "type": "request" - } - }, - { - "@timestamp": "2020-04-14T08:56:03.100Z", - "agent": { - "name": "elastic-node" - }, - "container": { - "id": "container-id" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-04-24T01:53:20.083059Z" - }, - "kubernetes": { - "pod": { - "name": "pod-name" - } - }, - "observer": { - "ephemeral_id": "fdbbb18c-f372-4b20-81c1-ab6596a0703a", - "hostname": "alloy", - "id": "fb7241d5-1c02-43dc-8561-1b8c5f95ad5c", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "processor": { - "event": "metric", - "name": "metric" - }, - "service": { - "environment": "staging", - "name": "1234_service-12a3", - "node": { - "name": "container-id" - }, - "version": "5.1.3" - }, - "timeseries": { - "instance": "1234_service-12a3:GET /api/types:2d9c304bd7ae1b2d" - }, - "transaction": { - "duration": { - "histogram": { - "counts": [ - 1 - ], - "values": [ - 14015 - ] - } - }, - "name": "GET /api/types", - "result": "failure", - "root": true, - "type": "request" - } - }, - { - "@timestamp": "2020-04-14T08:56:03.100Z", - "agent": { - "name": "js-base" - }, - "container": { - "id": "container-id" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-04-24T01:53:20.082443Z" - }, - "kubernetes": { - "pod": { - "name": "pod-name" - } - }, - "observer": { - "ephemeral_id": "fdbbb18c-f372-4b20-81c1-ab6596a0703a", - "hostname": "alloy", - "id": "fb7241d5-1c02-43dc-8561-1b8c5f95ad5c", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "processor": { - "event": "metric", - "name": "metric" - }, - "service": { - "environment": "staging", - "name": "serviceabc", - "node": { - "name": "container-id" - }, - "version": "5.1.3" - }, - "timeseries": { - "instance": "serviceabc:GET /api/types:eaedcae530dae5c2" - }, - "transaction": { - "duration": { - "histogram": { - "counts": [ - 1 - ], - "values": [ - 32639 - ] - } - }, - "name": "GET /api/types", - "result": "success", - "root": true, - "type": "request" - } - } -] \ No newline at end of file