Skip to content

Commit 3cdd9aa

Browse files
committed
Add feature gate for ES data stream migration
Implement jaeger.es.readLegacyWithDataStream feature gate and enhance ILM policy with forcemerge. Signed-off-by: SoumyaRaikwar <somuraik@gmail.com>
1 parent b1014d5 commit 3cdd9aa

30 files changed

+279
-713
lines changed

docs/design/elasticsearch-data-streams.md

Lines changed: 81 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,94 @@ This prefixing strategy prevents legacy wildcard queries like `jaeger-span-*` fr
2121

2222
### Index Templates
2323
Data stream-specific index templates are provided in `internal/storage/v1/elasticsearch/mappings/`:
24-
- `jaeger-ds-span-8.json`
25-
- `jaeger-ds-service-8.json`
26-
- `jaeger-ds-sampling-8.json`
27-
- `jaeger-ds-dependencies-8.json`
24+
- `jaeger-span-8.json` (merged with standard template)
25+
- `jaeger-service-8.json`
26+
- `jaeger-sampling-8.json`
27+
- `jaeger-dependencies-8.json`
2828

2929
These templates include the `data_stream: {}` field and specify a default ingest pipeline for timestamp normalization (e.g., `jaeger-ds-span-timestamp`).
3030

3131
### Storage Logic
3232
The storage stores (Span, Sampling, Dependency) are updated to:
33-
1. **Write** to the data stream name when `UseDataStream` is enabled.
34-
2. **Read** from both the data stream and the legacy wildcard patterns to ensure data visibility during the migration period.
33+
1. **Write** to the data stream name when `ES Version >= 8`.
34+
2. **Read** behavior is controlled by the `jaeger.es.readLegacyWithDataStream` feature gate:
35+
- When **enabled** (default during migration): Queries both data streams and legacy indices (e.g., `["jaeger-ds-span", "jaeger-span-*"]`)
36+
- When **disabled**: Queries only data streams (e.g., `["jaeger-ds-span"]`)
37+
38+
This feature gate allows operators to safely migrate from legacy indices to data streams and disable dual reads once migration is complete.
3539

3640
## Migration Strategy
3741
Jaeger supports a seamless migration from traditional indices to data streams:
38-
- **Phase 1**: Enable `UseDataStream`. Jaeger will start writing to the new `jaeger-ds-*` data streams.
39-
- **Phase 2**: During read operations, Jaeger queries both the new data streams and the legacy indices.
40-
- **Phase 3**: Once legacy indices are aged out and deleted, only data streams will be used.
42+
- **Phase 1**: Enable Data Streams (Default for ES 8+). Jaeger will start writing to the new `jaeger-ds-*` data streams.
43+
- **Phase 2**: During read operations, the `jaeger.es.readLegacyWithDataStream` feature gate (enabled by default) ensures Jaeger queries both the new data streams and the legacy indices.
44+
- **Phase 3**: Once legacy indices are aged out and deleted, disable the feature gate to query only data streams:
45+
```bash
46+
--feature-gates=-jaeger.es.readLegacyWithDataStream
47+
```
4148

4249
## Configuration
4350
The following configuration options control data stream usage:
44-
- `UseDataStream`: Boolean flag to enable data stream support (requires Elasticsearch 8+).
51+
- `ES Version`: Data streams are automatically enabled for Elasticsearch 8+.
4552
- `IndexPrefix`: Optional prefix for all indices and data streams.
53+
- `jaeger.es.readLegacyWithDataStream`: Feature gate to control dual reading (enabled by default for migration support).
54+
55+
## ILM Policy Proposal
56+
To manage the lifecycle of data streams effectively, we propose the following Index Lifecycle Management (ILM) policy, named `jaeger-ilm-policy`.
57+
58+
### Policy Definition
59+
The policy defines three phases to optimize storage costs and performance:
60+
61+
1. **Hot Phase**:
62+
- **Rollover**: Occurs when the index reaches **50GB**, **200 million documents**, or **1 day** of age.
63+
- **Priority**: Set to **100** (Highest).
64+
2. **Warm Phase**:
65+
- **Transition**: Moves to warm phase **0ms** after rollover (effectively immediately upon leaving hot).
66+
- **Forcemerge**: Reduces segment count to 1 for better search performance and reduced overhead.
67+
- **Priority**: Set to **50**.
68+
3. **Delete Phase**:
69+
- **Action**: Delete indices **7 days** after rollover.
70+
71+
### JSON Representation
72+
```json
73+
{
74+
"policy": {
75+
"phases": {
76+
"hot": {
77+
"min_age": "0ms",
78+
"actions": {
79+
"rollover": {
80+
"max_age": "1d",
81+
"max_primary_shard_size": "50gb",
82+
"max_docs": 200000000
83+
},
84+
"set_priority": {
85+
"priority": 100
86+
}
87+
}
88+
},
89+
"warm": {
90+
"min_age": "0ms",
91+
"actions": {
92+
"forcemerge": {
93+
"max_num_segments": 1
94+
},
95+
"set_priority": {
96+
"priority": 50
97+
}
98+
}
99+
},
100+
"delete": {
101+
"min_age": "7d",
102+
"actions": {
103+
"delete": {
104+
"delete_searchable_snapshot": true
105+
}
106+
}
107+
}
108+
}
109+
}
110+
}
111+
```
46112

47113
## Verification & Evidence
48114

@@ -74,10 +140,10 @@ curl -X PUT "localhost:9200/_ingest/pipeline/jaeger-ds-span-timestamp" -H 'Conte
74140

75141
### 2. Testing Steps
76142
1. **Prerequisites**: Elasticsearch 8.x and Jaeger built from the data stream support branch.
77-
2. **Configuration**: Set `use_data_stream: true` and `create_mappings: true` in the Jaeger configuration.
78-
3. **Verify Templates**: Ensure the `jaeger-ds-span` template exists and has `"data_stream": {}`.
143+
2. **Configuration**: Ensure `UseILM=true` (if applicable) or verify default templates.
144+
3. **Verify Templates**: Ensure the `jaeger-span` template exists and has `"data_stream": {}`.
79145
```bash
80-
curl -X GET "localhost:9200/_index_template/jaeger-ds-span?pretty"
146+
curl -X GET "localhost:9200/_index_template/jaeger-span?pretty"
81147
```
82148
4. **Generate Data**: Use `tracegen` or a sample app like HotROD to send spans.
83149
5. **Verify Data Stream**:
@@ -100,17 +166,5 @@ Documents are correctly indexed into the data stream with the `@timestamp` field
100166
}
101167
```
102168

103-
#### Backward Compatibility (Legacy Data)
104-
Existing indices (e.g., `jaeger-span-2025-12-27`) remain queryable alongside new data:
105-
```json
106-
{
107-
"_index": "jaeger-span-2025-12-27",
108-
"_source": {
109-
"traceID": "legacy-test-1",
110-
"startTimeMillis": 1735293600000
111-
}
112-
}
113-
```
114-
115-
#### Isolation Verified
116-
Confirmed that legacy wildcard searches like `jaeger-span-*` **do not** return data from `jaeger-ds-span`, ensuring complete isolation as requested.
169+
#### Verification
170+
Confirmed that traces are queryable and data streams are created as expected.

internal/storage/elasticsearch/client.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,8 @@ type IndexService interface {
5151
Index(index string) IndexService
5252
Type(typ string) IndexService
5353
Id(id string) IndexService
54-
OpType(opType string) IndexService
5554
BodyJson(body any) IndexService
56-
Add()
55+
Add(opType string)
5756
}
5857

5958
// SearchService is an abstraction for elastic.SearchService

internal/storage/elasticsearch/config/config.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,6 @@ type Configuration struct {
168168
// Read more about ILM at
169169
// https://www.jaegertracing.io/docs/deployment/#enabling-ilm-support
170170
UseILM bool `mapstructure:"use_ilm"`
171-
// UseDataStream, if set to true, will use Elasticsearch data streams for storing traces.
172-
// This requires Elasticsearch 7.9+.
173-
UseDataStream bool `mapstructure:"use_data_stream"`
174171

175172
// ---- jaeger-specific configs ----
176173
// MaxDocCount Defines maximum number of results to fetch from storage per query.
@@ -539,9 +536,6 @@ func (c *Configuration) ApplyDefaults(source *Configuration) {
539536
c.CustomHeaders[k] = v
540537
}
541538
}
542-
if !c.UseDataStream {
543-
c.UseDataStream = source.UseDataStream
544-
}
545539
}
546540

547541
// RolloverFrequencyAsNegativeDuration returns the index rollover frequency duration for the given frequency string

internal/storage/elasticsearch/mocks/mocks.go

Lines changed: 9 additions & 55 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/storage/elasticsearch/wrapper/wrapper.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -225,13 +225,11 @@ func (i IndexServiceWrapper) Type(typ string) es.IndexService {
225225
return WrapESIndexService(i.bulkIndexReq.Type(typ), i.bulkService, i.esVersion)
226226
}
227227

228-
// OpType calls this function to internal service.
229-
func (i IndexServiceWrapper) OpType(opType string) es.IndexService {
230-
return WrapESIndexService(i.bulkIndexReq.OpType(opType), i.bulkService, i.esVersion)
231-
}
232-
233228
// Add adds the request to bulk service
234-
func (i IndexServiceWrapper) Add() {
229+
func (i IndexServiceWrapper) Add(opType string) {
230+
if opType != "" {
231+
i.bulkIndexReq.OpType(opType)
232+
}
235233
i.bulkService.Add(i.bulkIndexReq)
236234
}
237235

internal/storage/v1/elasticsearch/factory.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ func (f *FactoryBase) GetSpanReaderParams() esspanstore.SpanReaderParams {
121121
ServiceReadAlias: f.config.ServiceReadAlias,
122122
Logger: f.logger,
123123
Tracer: f.tracer.Tracer("esspanstore.SpanReader"),
124-
UseDataStream: f.config.UseDataStream,
125124
}
126125
}
127126

@@ -142,7 +141,6 @@ func (f *FactoryBase) GetSpanWriterParams() esspanstore.SpanWriterParams {
142141
Logger: f.logger,
143142
MetricsFactory: f.metricsFactory,
144143
ServiceCacheTTL: f.config.ServiceCacheTTL,
145-
UseDataStream: f.config.UseDataStream,
146144
}
147145
}
148146

@@ -155,7 +153,6 @@ func (f *FactoryBase) GetDependencyStoreParams() esdepstorev2.Params {
155153
IndexDateLayout: f.config.Indices.Dependencies.DateLayout,
156154
MaxDocCount: f.config.MaxDocCount,
157155
UseReadWriteAliases: f.config.UseReadWriteAliases,
158-
UseDataStream: f.config.UseDataStream,
159156
}
160157
}
161158

@@ -168,7 +165,6 @@ func (f *FactoryBase) CreateSamplingStore(int /* maxBuckets */) (samplingstore.S
168165
IndexRolloverFrequency: config.RolloverFrequencyAsNegativeDuration(f.config.Indices.Sampling.RolloverFrequency),
169166
Lookback: f.config.AdaptiveSamplingLookback,
170167
MaxDocCount: f.config.MaxDocCount,
171-
UseDataStream: f.config.UseDataStream,
172168
}
173169
store := essamplestore.NewSamplingStore(params)
174170

@@ -194,7 +190,6 @@ func (f *FactoryBase) mappingBuilderFromConfig(cfg *config.Configuration) mappin
194190
Indices: cfg.Indices,
195191
EsVersion: cfg.Version,
196192
UseILM: cfg.UseILM,
197-
UseDataStream: cfg.UseDataStream,
198193
ILMPolicyName: cfg.Indices.IndexPrefix.Apply(defaultILMPolicyName),
199194
}
200195
}

internal/storage/v1/elasticsearch/mappings/fixtures/jaeger-dependencies-8.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"priority": 502,
3-
"index_patterns": "test-jaeger-dependencies-*",
3+
"index_patterns": "test-jaeger-dependencies",
4+
"data_stream": {},
45
"template": {
56
"aliases": {
67
"test-jaeger-dependencies-read": {}

0 commit comments

Comments
 (0)