Skip to content
This repository was archived by the owner on Nov 7, 2025. It is now read-only.

Commit d59a9f2

Browse files
authored
Don't remove Elasticsarch indices/datastreams from _resolve endpoint (#989)
I guess this is due to some historical reasons which are no longer accurate. After this patch, you can create data stream with ES index without issues: <img width="600" alt="image" src="https://github.com/user-attachments/assets/d6b48cdd-77fb-47ee-82d3-eedb5b0c5031"> At this moment, we simply return all(*) the sources. After all, created data view may outlive Quesma and related routing configuration. (*) all the Elasticsearch indices/datastreams/aliases matching desired pattern plus our ClickHouse tables presented as Data Streams. Closes: #996
1 parent 0b40e2c commit d59a9f2

File tree

4 files changed

+93
-60
lines changed

4 files changed

+93
-60
lines changed

ci/it/configs/quesma-with-dual-writes-and-common-table.yml.template

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ processors:
3030
target: [ c ]
3131
logs-3:
3232
target: [ c, e ]
33-
logs-dual-query:
33+
## WARNING `logs-dual-query` (with two dashes) falls under default index pattern for logs in Elasticsearch and results in not index, but datastream creation
34+
logs-dual_query:
3435
target: [ c, e ]
3536
logs-4:
3637
target:
@@ -52,7 +53,7 @@ processors:
5253
target: [ c ]
5354
logs-3:
5455
target: [ c, e ]
55-
logs-dual-query:
56+
logs-dual_query:
5657
target: [ c, e ]
5758
logs-4:
5859
target:

ci/it/testcases/test_dual_write_and_common_table.go

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ func (a *DualWriteAndCommonTableTestcase) RunTests(ctx context.Context, t *testi
4040
t.Run("test dual query returns data from clickhouse", func(t *testing.T) { a.testDualQueryReturnsDataFromClickHouse(ctx, t) })
4141
t.Run("test dual writes work", func(t *testing.T) { a.testDualWritesWork(ctx, t) })
4242
t.Run("test wildcard goes to elastic", func(t *testing.T) { a.testWildcardGoesToElastic(ctx, t) })
43+
t.Run("test _resolve/index/* works properly", func(t *testing.T) { a.testResolveEndpointInQuesma(ctx, t) })
4344
return nil
4445
}
4546

@@ -100,10 +101,10 @@ func (a *DualWriteAndCommonTableTestcase) testIngestToCommonTableWorks(ctx conte
100101
}
101102

102103
func (a *DualWriteAndCommonTableTestcase) testDualQueryReturnsDataFromClickHouse(ctx context.Context, t *testing.T) {
103-
resp, _ := a.RequestToQuesma(ctx, t, "POST", "/logs-dual-query/_doc", []byte(`{"name": "Przemyslaw", "age": 31337}`))
104+
resp, _ := a.RequestToQuesma(ctx, t, "POST", "/logs-dual_query/_doc", []byte(`{"name": "Przemyslaw", "age": 31337}`))
104105
assert.Equal(t, http.StatusOK, resp.StatusCode)
105106

106-
chQuery := "SELECT * FROM 'logs-dual-query'"
107+
chQuery := "SELECT * FROM 'logs-dual_query'"
107108
rows, err := a.ExecuteClickHouseQuery(ctx, chQuery)
108109
if err != nil {
109110
t.Fatalf("Failed to execute query: %s", err)
@@ -140,12 +141,12 @@ func (a *DualWriteAndCommonTableTestcase) testDualQueryReturnsDataFromClickHouse
140141
assert.Equal(t, 31337, age)
141142

142143
// In the meantime let's delete the index from Elasticsearch
143-
_, _ = a.RequestToElasticsearch(ctx, "DELETE", "/logs-dual-query", nil)
144+
resp, _ = a.RequestToElasticsearch(ctx, "DELETE", "/logs-dual_query", nil)
144145
if err != nil {
145146
t.Fatalf("Failed to make DELETE request: %s", err)
146147
}
147148
// FINAL TEST - WHETHER QUESMA RETURNS DATA FROM CLICKHOUSE
148-
resp, bodyBytes := a.RequestToQuesma(ctx, t, "GET", "/logs-dual-query/_search", []byte(`{"query": {"match_all": {}}}`))
149+
resp, bodyBytes := a.RequestToQuesma(ctx, t, "GET", "/logs-dual_query/_search", []byte(`{"query": {"match_all": {}}}`))
149150
assert.Equal(t, http.StatusOK, resp.StatusCode)
150151
assert.Contains(t, string(bodyBytes), "Przemyslaw")
151152
assert.Contains(t, "Clickhouse", resp.Header.Get("X-Quesma-Source"))
@@ -291,3 +292,58 @@ func (a *DualWriteAndCommonTableTestcase) testWildcardGoesToElastic(ctx context.
291292
assert.Equal(t, "Elasticsearch", resp.Header.Get("X-Quesma-Source"))
292293
assert.Equal(t, "Elasticsearch", resp.Header.Get("X-Elastic-Product"))
293294
}
295+
296+
func (a *DualWriteAndCommonTableTestcase) testResolveEndpointInQuesma(ctx context.Context, t *testing.T) {
297+
// When Quesma searches for that document
298+
resp, bodyBytes := a.RequestToQuesma(ctx, t, "GET", "/_resolve/index/*", nil)
299+
300+
var jsonResponse map[string]interface{}
301+
if err := json.Unmarshal(bodyBytes, &jsonResponse); err != nil {
302+
t.Fatalf("Failed to unmarshal response body: %s", err)
303+
}
304+
305+
assert.Equal(t, http.StatusOK, resp.StatusCode)
306+
assert.Equal(t, "Clickhouse", resp.Header.Get("X-Quesma-Source"))
307+
expectedResponse := map[string]interface{}{
308+
"indices": []interface{}{
309+
map[string]interface{}{
310+
"name": "logs-3",
311+
"attributes": []interface{}{"open"},
312+
},
313+
map[string]interface{}{
314+
"name": "quesma_virtual_tables",
315+
"attributes": []interface{}{"open"},
316+
},
317+
map[string]interface{}{
318+
"name": "unmentioned_index",
319+
"attributes": []interface{}{"open"},
320+
},
321+
},
322+
"aliases": []interface{}{},
323+
"data_streams": []interface{}{
324+
map[string]interface{}{
325+
"name": "logs-2",
326+
"backing_indices": []interface{}{"logs-2"},
327+
"timestamp_field": "@timestamp",
328+
},
329+
map[string]interface{}{
330+
"name": "logs-3",
331+
"backing_indices": []interface{}{"logs-3"},
332+
"timestamp_field": "@timestamp",
333+
},
334+
map[string]interface{}{
335+
"name": "logs-4",
336+
"backing_indices": []interface{}{"logs-4"},
337+
"timestamp_field": "@timestamp",
338+
},
339+
map[string]interface{}{
340+
"name": "logs-dual_query",
341+
"backing_indices": []interface{}{"logs-dual_query"},
342+
"timestamp_field": "@timestamp",
343+
},
344+
},
345+
}
346+
assert.ElementsMatchf(t, expectedResponse["indices"], jsonResponse["indices"], "indices do not match")
347+
assert.ElementsMatchf(t, expectedResponse["aliases"], jsonResponse["aliases"], "aliases do not match")
348+
assert.ElementsMatchf(t, expectedResponse["data_streams"], jsonResponse["data_streams"], "data_streams do not match")
349+
}

quesma/quesma/functionality/resolve/resolve.go

Lines changed: 26 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,73 +4,48 @@ package resolve
44

55
import (
66
"quesma/elasticsearch"
7+
"quesma/logger"
78
"quesma/quesma/config"
89
"quesma/schema"
9-
"slices"
1010
)
1111

12+
// HandleResolve combines results from both schema.Registry (ClickHouse) and Elasticsearch,
13+
// This endpoint is used in Kibana/OSD when creating Data Views/Index Patterns.
1214
func HandleResolve(pattern string, sr schema.Registry, cfg *config.QuesmaConfiguration) (elasticsearch.Sources, error) {
13-
// In the _resolve endpoint we want to combine the results from both schema.Registry and Elasticsearch
15+
sourcesToShow := &elasticsearch.Sources{}
1416

15-
normalizedPattern := elasticsearch.NormalizePattern(pattern)
17+
normalizedPattern := elasticsearch.NormalizePattern(pattern) // changes `_all` to `*`
1618

17-
// Optimization: if it's not a pattern, let's try avoiding querying Elasticsearch - let's first try
18-
// finding that index in schema.Registry:
19-
if !elasticsearch.IsIndexPattern(normalizedPattern) {
20-
if foundSchema, found := sr.FindSchema(schema.TableName(normalizedPattern)); found {
21-
if !foundSchema.ExistsInDataSource {
22-
// index configured by the user, but not present in the data source
23-
return elasticsearch.Sources{}, nil
24-
}
25-
26-
return elasticsearch.Sources{
27-
Indices: []elasticsearch.Index{},
28-
Aliases: []elasticsearch.Alias{},
29-
DataStreams: []elasticsearch.DataStream{
30-
{
31-
Name: normalizedPattern,
32-
BackingIndices: []string{normalizedPattern},
33-
TimestampField: `@timestamp`,
34-
},
35-
},
36-
}, nil
37-
}
38-
39-
// ...index not found in schema.Registry (meaning the user did not configure it), but it could exist in Elastic
40-
}
41-
42-
// Combine results from both schema.Registry and Elasticsearch:
43-
44-
// todo avoid creating new instances all the time
45-
sourcesFromElastic, _, err := elasticsearch.NewIndexResolver(cfg.Elasticsearch).Resolve(normalizedPattern)
19+
sourcesFromElasticsearch, _, err := elasticsearch.NewIndexResolver(cfg.Elasticsearch).Resolve(normalizedPattern)
4620
if err != nil {
47-
return elasticsearch.Sources{}, err
21+
logger.Warn().Msgf("Failed fetching resolving sources matching `%s`: %v", pattern, err)
22+
} else {
23+
sourcesToShow = &sourcesFromElasticsearch
4824
}
4925

50-
combineSourcesFromElasticWithRegistry(&sourcesFromElastic, sr.AllSchemas(), normalizedPattern)
51-
return sourcesFromElastic, nil
52-
}
26+
tablesFromClickHouse := getMatchingClickHouseTables(sr.AllSchemas(), normalizedPattern)
5327

54-
func combineSourcesFromElasticWithRegistry(sourcesFromElastic *elasticsearch.Sources, schemas map[schema.TableName]schema.Schema, normalizedPattern string) {
55-
sourcesFromElastic.Indices =
56-
slices.DeleteFunc(sourcesFromElastic.Indices, func(i elasticsearch.Index) bool {
57-
_, exists := schemas[schema.TableName(i.Name)]
58-
return exists
59-
})
60-
sourcesFromElastic.DataStreams = slices.DeleteFunc(sourcesFromElastic.DataStreams, func(i elasticsearch.DataStream) bool {
61-
_, exists := schemas[schema.TableName(i.Name)]
62-
return exists
63-
})
28+
addClickHouseTablesToSourcesFromElastic(sourcesToShow, tablesFromClickHouse)
29+
return *sourcesToShow, nil
30+
}
6431

32+
func getMatchingClickHouseTables(schemas map[schema.TableName]schema.Schema, normalizedPattern string) (tables []string) {
6533
for name, currentSchema := range schemas {
6634
indexName := name.AsString()
6735

6836
if config.MatchName(normalizedPattern, indexName) && currentSchema.ExistsInDataSource {
69-
sourcesFromElastic.DataStreams = append(sourcesFromElastic.DataStreams, elasticsearch.DataStream{
70-
Name: indexName,
71-
BackingIndices: []string{indexName},
72-
TimestampField: `@timestamp`,
73-
})
37+
tables = append(tables, indexName)
7438
}
7539
}
40+
return tables
41+
}
42+
43+
func addClickHouseTablesToSourcesFromElastic(sourcesFromElastic *elasticsearch.Sources, chTableNames []string) {
44+
for _, name := range chTableNames { // Quesma presents CH tables as Elasticsearch Data Streams.
45+
sourcesFromElastic.DataStreams = append(sourcesFromElastic.DataStreams, elasticsearch.DataStream{
46+
Name: name,
47+
BackingIndices: []string{name},
48+
TimestampField: `@timestamp`,
49+
})
50+
}
7651
}

quesma/quesma/functionality/resolve/resolve_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func Test_combineSourcesFromElasticWithRegistry(t *testing.T) {
6060
},
6161
normalizedPattern: "index*",
6262
expectedResult: elasticsearch.Sources{
63-
Indices: []elasticsearch.Index{{Name: "index3"}},
63+
Indices: []elasticsearch.Index{{Name: "index1"}, {Name: "index3"}},
6464
Aliases: []elasticsearch.Alias{},
6565
DataStreams: []elasticsearch.DataStream{},
6666
},
@@ -81,9 +81,10 @@ func Test_combineSourcesFromElasticWithRegistry(t *testing.T) {
8181
},
8282
normalizedPattern: "index*",
8383
expectedResult: elasticsearch.Sources{
84-
Indices: []elasticsearch.Index{{Name: "index4"}},
84+
Indices: []elasticsearch.Index{{Name: "index1"}, {Name: "index4"}},
8585
Aliases: []elasticsearch.Alias{},
8686
DataStreams: []elasticsearch.DataStream{
87+
{Name: "index3"},
8788
{Name: "index5"},
8889
{Name: "index1", BackingIndices: []string{"index1"}, TimestampField: `@timestamp`},
8990
{Name: "index2", BackingIndices: []string{"index2"}, TimestampField: `@timestamp`},
@@ -95,7 +96,7 @@ func Test_combineSourcesFromElasticWithRegistry(t *testing.T) {
9596

9697
for _, tt := range tests {
9798
t.Run(tt.name, func(t *testing.T) {
98-
combineSourcesFromElasticWithRegistry(&tt.sourcesFromElastic, tt.schemas, tt.normalizedPattern)
99+
addClickHouseTablesToSourcesFromElastic(&tt.sourcesFromElastic, getMatchingClickHouseTables(tt.schemas, tt.normalizedPattern))
99100
assert.ElementsMatchf(t, tt.sourcesFromElastic.Aliases, tt.expectedResult.Aliases, "Aliases don't match")
100101
assert.ElementsMatchf(t, tt.sourcesFromElastic.Indices, tt.expectedResult.Indices, "Indices don't match")
101102
assert.ElementsMatchf(t, tt.sourcesFromElastic.DataStreams, tt.expectedResult.DataStreams, "DataStreams don't match")

0 commit comments

Comments
 (0)