Skip to content

Commit b0b8e85

Browse files
belimawrpchila
andauthored
Support flattened data_stream.* fields (#3465)
An input configuration supports flattened fields, however the 'data_stream' field was not being correctly decoded when flattened. This commit fixes this issue. Some small additions and refactoring are also implemented in the integration test framework as well as some more detailed documentation. --------- Co-authored-by: Paolo Chilà <paolo.chila@elastic.co>
1 parent 29386eb commit b0b8e85

File tree

9 files changed

+692
-221
lines changed

9 files changed

+692
-221
lines changed

.buildkite/pipeline.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ steps:
140140
key: "serverless-integration-tests"
141141
env:
142142
TEST_INTEG_AUTH_ESS_REGION: us-east-1
143-
command: ".buildkite/scripts/steps/integration_tests.sh serverless integration:single TestMonitoringLogsShipped" #right now, run a single test in serverless mode as a sort of smoke test, instead of re-running the entire suite
143+
command: ".buildkite/scripts/steps/integration_tests.sh serverless integration:single TestLogIngestionFleetManaged" #right now, run a single test in serverless mode as a sort of smoke test, instead of re-running the entire suite
144144
artifact_paths:
145145
- "build/TEST-**"
146146
- "build/diagnostics/*"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Kind can be one of:
2+
# - breaking-change: a change to previously-documented behavior
3+
# - deprecation: functionality that is being removed in a later release
4+
# - bug-fix: fixes a problem in a previous version
5+
# - enhancement: extends functionality but does not break or fix existing behavior
6+
# - feature: new functionality
7+
# - known-issue: problems that we are aware of in a given version
8+
# - security: impacts on the security of a product or a user’s deployment.
9+
# - upgrade: important information for someone upgrading from a prior version
10+
# - other: does not fit into any of the other categories
11+
kind: feature
12+
13+
# Change summary; a 80ish characters long description of the change.
14+
summary: Support flattened data_stream.* fields in input configuration
15+
16+
# Long description; in case the summary is not enough to describe the change
17+
# this field accommodate a description without length limits.
18+
# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment.
19+
description: >-
20+
An input configuration supports flattened fields, however the
21+
'data_stream' field was not being correctly decoded when
22+
flattened. This commit fixes this issue.
23+
24+
# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc.
25+
component: elastic-agent
26+
27+
# PR URL; optional; the PR number that added the changeset.
28+
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added.
29+
# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number.
30+
# Please provide it if you are adding a fragment for a different PR.
31+
pr: https://github.com/elastic/elastic-agent/pull/3465
32+
33+
# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
34+
# If not present is automatically filled by the tooling with the issue linked to the PR number.
35+
issue: https://github.com/elastic/elastic-agent/issues/3191

pkg/component/component_test.go

+71
Original file line numberDiff line numberDiff line change
@@ -2361,3 +2361,74 @@ func gatherDurationFieldPaths(s interface{}, pathSoFar string) []string {
23612361

23622362
return gatheredPaths
23632363
}
2364+
2365+
func TestFlattenedDataStream(t *testing.T) {
2366+
expectedNamespace := "test-namespace"
2367+
expectedType := "test-type"
2368+
expectedDataset := "test-dataset"
2369+
2370+
policy := map[string]any{
2371+
"outputs": map[string]any{
2372+
"default": map[string]any{
2373+
"type": "elasticsearch",
2374+
"enabled": true,
2375+
},
2376+
},
2377+
"inputs": []any{
2378+
map[string]any{
2379+
"type": "filestream",
2380+
"id": "filestream-0",
2381+
"enabled": true,
2382+
"data_stream.type": expectedType,
2383+
"data_stream.dataset": expectedDataset,
2384+
"data_stream": map[string]any{
2385+
"namespace": expectedNamespace,
2386+
},
2387+
},
2388+
},
2389+
}
2390+
runtime, err := LoadRuntimeSpecs(filepath.Join("..", "..", "specs"), PlatformDetail{}, SkipBinaryCheck())
2391+
if err != nil {
2392+
t.Fatalf("cannot load runtime specs: %s", err)
2393+
}
2394+
2395+
result, err := runtime.ToComponents(policy, nil, logp.DebugLevel, nil)
2396+
if err != nil {
2397+
t.Fatalf("cannot convert policy to component: %s", err)
2398+
}
2399+
2400+
if len(result) != 1 {
2401+
t.Fatalf("expecting result to have one element, got %d", len(result))
2402+
}
2403+
2404+
if len(result[0].Units) != 2 {
2405+
t.Fatalf("expecting result[0].Units to have two elements, got %d", len(result))
2406+
}
2407+
2408+
// We do not make assumptions about ordering.
2409+
// Get the input Unit
2410+
var dataStream *proto.DataStream
2411+
for _, unit := range result[0].Units {
2412+
if unit.Err != nil {
2413+
t.Fatalf("unit.Err: %s", unit.Err)
2414+
}
2415+
if unit.Type == client.UnitTypeInput {
2416+
dataStream = unit.Config.DataStream
2417+
break
2418+
}
2419+
}
2420+
2421+
if dataStream == nil {
2422+
t.Fatal("DataStream cannot be nil")
2423+
}
2424+
2425+
if dataStream.Dataset != expectedDataset {
2426+
t.Errorf("expecting DataStream.Dataset: %q, got: %q", expectedDataset, dataStream.Dataset)
2427+
}
2428+
if dataStream.Type != expectedType {
2429+
t.Errorf("expecting DataStream.Type: %q, got: %q", expectedType, dataStream.Type)
2430+
}
2431+
if dataStream.Namespace != expectedNamespace {
2432+
t.Errorf("expecting DataStream.Namespace: %q, got: %q", expectedNamespace, dataStream.Namespace)
2433+
}
2434+
}

pkg/component/config.go

+80
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"google.golang.org/protobuf/types/known/structpb"
1616

1717
"github.com/elastic/elastic-agent-client/v7/pkg/proto"
18+
"github.com/elastic/elastic-agent-libs/config"
1819
"github.com/elastic/elastic-agent/pkg/limits"
1920
)
2021

@@ -100,9 +101,88 @@ func ExpectedConfig(cfg map[string]interface{}) (*proto.UnitExpectedConfig, erro
100101
return nil, err
101102
}
102103

104+
if err := updateDataStreamsFromSource(result); err != nil {
105+
return nil, fmt.Errorf("could not dedot 'data_stream': %w", err)
106+
}
107+
103108
return result, nil
104109
}
105110

111+
func deDotDataStream(ds *proto.DataStream, source *structpb.Struct) (*proto.DataStream, error) {
112+
if ds == nil {
113+
ds = &proto.DataStream{}
114+
}
115+
116+
cfg, err := config.NewConfigFrom(source.AsMap())
117+
if err != nil {
118+
return nil, fmt.Errorf("cannot generate config from source field: %w", err)
119+
}
120+
121+
// Create a temporary struct to unpack the configuration.
122+
// Unpack correctly handles any flattened fields like
123+
// data_stream.type. So all we need to do is to call Unpack,
124+
// ensure the DataStream does not have a different value,
125+
// them merge them both.
126+
tmp := struct {
127+
DataStream struct {
128+
Dataset string `config:"dataset" yaml:"dataset"`
129+
Type string `config:"type" yaml:"type"`
130+
Namespace string `config:"namespace" yaml:"namespace"`
131+
} `config:"data_stream" yaml:"data_stream"`
132+
}{}
133+
134+
if err := cfg.Unpack(&tmp); err != nil {
135+
return nil, fmt.Errorf("cannot unpack source field into struct: %w", err)
136+
}
137+
138+
if (ds.Dataset != tmp.DataStream.Dataset) && (ds.Dataset != "" && tmp.DataStream.Dataset != "") {
139+
return nil, errors.New("duplicated key 'datastream.dataset'")
140+
}
141+
142+
if (ds.Type != tmp.DataStream.Type) && (ds.Type != "" && tmp.DataStream.Type != "") {
143+
return nil, errors.New("duplicated key 'datastream.type'")
144+
}
145+
146+
if (ds.Namespace != tmp.DataStream.Namespace) && (ds.Namespace != "" && tmp.DataStream.Namespace != "") {
147+
return nil, errors.New("duplicated key 'datastream.namespace'")
148+
}
149+
150+
ret := &proto.DataStream{
151+
Dataset: valueOrDefault(tmp.DataStream.Dataset, ds.Dataset),
152+
Type: valueOrDefault(tmp.DataStream.Type, ds.Type),
153+
Namespace: valueOrDefault(tmp.DataStream.Namespace, ds.Namespace),
154+
Source: ds.GetSource(),
155+
}
156+
157+
return ret, nil
158+
}
159+
160+
// valueOrDefault returns b if a is an empty string
161+
func valueOrDefault(a, b string) string {
162+
if a == "" {
163+
return b
164+
}
165+
return a
166+
}
167+
168+
func updateDataStreamsFromSource(unitConfig *proto.UnitExpectedConfig) error {
169+
var err error
170+
unitConfig.DataStream, err = deDotDataStream(unitConfig.GetDataStream(), unitConfig.GetSource())
171+
if err != nil {
172+
return fmt.Errorf("could not parse data_stream from input: %w", err)
173+
}
174+
175+
for i, stream := range unitConfig.Streams {
176+
stream.DataStream, err = deDotDataStream(stream.GetDataStream(), stream.GetSource())
177+
if err != nil {
178+
return fmt.Errorf("could not parse data_stream from stream [%d]: %w",
179+
i, err)
180+
}
181+
}
182+
183+
return nil
184+
}
185+
106186
func setSource(val interface{}, cfg map[string]interface{}) error {
107187
// find the source field on the val
108188
resVal := reflect.ValueOf(val).Elem()

pkg/component/config_test.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import (
88
"errors"
99
"testing"
1010

11+
"github.com/google/go-cmp/cmp"
1112
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/require"
14+
"google.golang.org/protobuf/testing/protocmp"
1315
"google.golang.org/protobuf/types/known/structpb"
1416

1517
"github.com/elastic/elastic-agent-client/v7/pkg/proto"
@@ -197,7 +199,12 @@ func TestExpectedConfig(t *testing.T) {
197199
assert.Equal(t, err.Error(), scenario.Err.Error())
198200
} else {
199201
require.NoError(t, err)
200-
assert.EqualValues(t, scenario.Expected, observed)
202+
// protocmp.Transform ensures we do not compare any internal
203+
// protobuf fields
204+
if !cmp.Equal(scenario.Expected, observed, protocmp.Transform()) {
205+
t.Errorf("mismatch (-want +got) \n%s",
206+
cmp.Diff(scenario.Expected, observed, protocmp.Transform()))
207+
}
201208
}
202209
})
203210
}

pkg/testing/fixture.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,14 @@ func (f *Fixture) RunBeat(ctx context.Context) error {
357357
// Elastic Agent is stopped. If at any time the Elastic Agent logs an error log and the Fixture is not started
358358
// with `WithAllowErrors()` then `Run` will exit early and return the logged error.
359359
//
360-
// If no `states` are provided then the Elastic Agent runs until the context is or the timeout specified with WithRunLength is reached.
360+
// If no `states` are provided then the Elastic Agent runs until the context is cancelled.
361+
//
362+
// The Elastic-Agent is started agent in test mode (--testing-mode) this mode
363+
// expects the initial configuration (full YAML config) via gRPC.
364+
// This configuration should be passed in the State.Configure field.
365+
//
366+
// The `elastic-agent.yml` generated by `Fixture.Configure` is ignored
367+
// when `Run` is called.
361368
func (f *Fixture) Run(ctx context.Context, states ...State) error {
362369
if f.binaryName != "elastic-agent" {
363370
return errors.New("Run() can only be used with elastic-agent, use RunBeat()")

pkg/testing/tools/estools/elasticsearch.go

+59-5
Original file line numberDiff line numberDiff line change
@@ -438,9 +438,9 @@ func CheckForErrorsInLogsWithContext(ctx context.Context, client elastictranspor
438438

439439
}
440440

441-
// GetLogsForDatastream returns any logs associated with the datastream
442-
func GetLogsForDatastream(client elastictransport.Interface, index string) (Documents, error) {
443-
return GetLogsForDatastreamWithContext(context.Background(), client, index)
441+
// GetLogsForDataset returns any logs associated with the datastream
442+
func GetLogsForDataset(client elastictransport.Interface, index string) (Documents, error) {
443+
return GetLogsForDatasetWithContext(context.Background(), client, index)
444444
}
445445

446446
// GetLogsForAgentID returns any logs associated with the agent ID
@@ -478,8 +478,8 @@ func GetLogsForAgentID(client elastictransport.Interface, id string) (Documents,
478478
return handleDocsResponse(res)
479479
}
480480

481-
// GetLogsForDatastreamWithContext returns any logs associated with the datastream
482-
func GetLogsForDatastreamWithContext(ctx context.Context, client elastictransport.Interface, index string) (Documents, error) {
481+
// GetLogsForDatasetWithContext returns any logs associated with the datastream
482+
func GetLogsForDatasetWithContext(ctx context.Context, client elastictransport.Interface, index string) (Documents, error) {
483483
indexQuery := map[string]interface{}{
484484
"query": map[string]interface{}{
485485
"match": map[string]interface{}{
@@ -536,6 +536,60 @@ func performQueryForRawQuery(ctx context.Context, queryRaw map[string]interface{
536536
return handleDocsResponse(res)
537537
}
538538

539+
// GetLogsForDatastream returns any logs associated with the datastream
540+
func GetLogsForDatastream(
541+
ctx context.Context,
542+
client elastictransport.Interface,
543+
dsType, dataset, namespace string) (Documents, error) {
544+
545+
query := map[string]any{
546+
"_source": []string{"message"},
547+
"query": map[string]any{
548+
"bool": map[string]any{
549+
"must": []any{
550+
map[string]any{
551+
"match": map[string]any{
552+
"data_stream.dataset": dataset,
553+
},
554+
},
555+
map[string]any{
556+
"match": map[string]any{
557+
"data_stream.namespace": namespace,
558+
},
559+
},
560+
map[string]any{
561+
"match": map[string]any{
562+
"data_stream.type": dsType,
563+
},
564+
},
565+
},
566+
},
567+
},
568+
}
569+
570+
var buf bytes.Buffer
571+
if err := json.NewEncoder(&buf).Encode(query); err != nil {
572+
return Documents{}, fmt.Errorf("error creating ES query: %w", err)
573+
}
574+
575+
es := esapi.New(client)
576+
res, err := es.Search(
577+
es.Search.WithIndex(fmt.Sprintf(".ds-%s*", dsType)),
578+
es.Search.WithExpandWildcards("all"),
579+
es.Search.WithBody(&buf),
580+
es.Search.WithTrackTotalHits(true),
581+
es.Search.WithPretty(),
582+
es.Search.WithContext(ctx),
583+
)
584+
if err != nil {
585+
return Documents{}, fmt.Errorf("error performing ES search: %w", err)
586+
}
587+
588+
return handleDocsResponse(res)
589+
}
590+
591+
// handleDocsResponse converts the esapi.Response into Documents,
592+
// it closes the response.Body after reading
539593
func handleDocsResponse(res *esapi.Response) (Documents, error) {
540594
resultBuf, err := handleResponseRaw(res)
541595
if err != nil {

0 commit comments

Comments
 (0)