Skip to content

Commit 136c423

Browse files
Preserve pre-existing rally-metrics index templates by default (#1912)
It handles issue #1900: Rally should not overwrite pre existing templates by default When opening an EsMetricsStore, it creates the index template if any of following is true: - the index template doesn't exist - `reporting/datastore.overwrite_existing_templates` option is true and there are differences between existing and requested template. It will preserve existing template on all the other cases. It logs a warning when an existing index template is being replaced. It logs index template differences between the existing one (if any) and configured one.
1 parent 76aca76 commit 136c423

7 files changed

Lines changed: 154 additions & 8 deletions

File tree

docs/configuration.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ The following settings are applicable only if ``datastore.type`` is set to "elas
8080
* ``datastore.probe.cluster_version`` (default: true): Enables automatic detection of the metric store's version.
8181
* ``datastore.number_of_shards`` (default: `Elasticsearch default value <https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#_static_index_settings>`_): The number of primary shards that the ``rally-*`` indices should have. Any updates to this setting after initial index creation will only be applied to new ``rally-*`` indices.
8282
* ``datastore.number_of_replicas`` (default: `Elasticsearch default value <https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#_static_index_settings>`_): The number of replicas each primary shard has. Defaults to . Any updates to this setting after initial index creation will only be applied to new ``rally-*`` indices.
83+
* ``datastore.overwrite_existing_templates`` (default: ``false``): Existing Rally index templates are replaced only when this option is ``true``.
84+
8385

8486
**Examples**
8587

docs/migrate.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ Minimum Python version is 3.9.0
99

1010
Rally 2.12.0 requires Python 3.9.0 or above. Check the :ref:`updated installation instructions <install_python>` for more details.
1111

12+
The metrics store keeps existing index templates
13+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14+
15+
Existing Rally index templates are replaced only when option ``datastore.overwrite_existing_templates`` in section ``reporting`` is ``true``.
16+
17+
1218
Migrating to Rally 2.10.1
1319
-------------------------
1420

esrally/metrics.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# KIND, either express or implied. See the License for the
1515
# specific language governing permissions and limitations
1616
# under the License.
17+
from __future__ import annotations
1718

1819
import collections
1920
import datetime
@@ -33,7 +34,7 @@
3334
import tabulate
3435

3536
from esrally import client, config, exceptions, paths, time, types, version
36-
from esrally.utils import console, convert, io, versions
37+
from esrally.utils import console, convert, io, pretty, versions
3738

3839

3940
class EsClient:
@@ -47,6 +48,9 @@ def __init__(self, client, cluster_version=None):
4748
self._cluster_version = cluster_version
4849
self.retryable_status_codes = [502, 503, 504, 429]
4950

51+
def get_template(self, name):
52+
return self.guarded(self._client.indices.get_index_template, name=name)
53+
5054
def put_template(self, name, template):
5155
tmpl = json.loads(template)
5256
return self.guarded(self._client.indices.put_index_template, name=name, **tmpl)
@@ -898,8 +902,7 @@ def open(self, race_id=None, race_timestamp=None, track_name=None, challenge_nam
898902
self._index = self.index_name()
899903
# reduce a bit of noise in the metrics cluster log
900904
if create:
901-
# always update the mapping to the latest version
902-
self._client.put_template("rally-metrics", self._get_template())
905+
self._ensure_index_template()
903906
if not self._client.exists(index=self._index):
904907
self._client.create_index(index=self._index)
905908
else:
@@ -913,6 +916,34 @@ def open(self, race_id=None, race_timestamp=None, track_name=None, challenge_nam
913916
# ensure we can search immediately after opening
914917
self._client.refresh(index=self._index)
915918

919+
def _ensure_index_template(self):
920+
new_template: str = self._get_template()
921+
922+
old_template: dict | None = None
923+
if self._client.template_exists("rally-metrics"):
924+
for t in self._client.get_template("rally-metrics").body.get("index_templates", []):
925+
old_template = t.get("index_template", {}).get("template", {})
926+
break
927+
928+
if old_template is None:
929+
self.logger.info(
930+
"Create index template:\n%s",
931+
pretty.dump(json.loads(new_template).get("template", {}), pretty.Flag.FLAT_DICT),
932+
)
933+
else:
934+
diff = pretty.diff(old_template, json.loads(new_template).get("template", {}), pretty.Flag.FLAT_DICT)
935+
if diff == "":
936+
self.logger.debug("Keep existing template (it is identical)")
937+
return
938+
if not convert.to_bool(
939+
self._config.opts(section="reporting", key="datastore.overwrite_existing_templates", default_value=False, mandatory=False)
940+
):
941+
self.logger.debug("Keep existing template (datastore.overwrite_existing_templates = false):\n%s", diff)
942+
return
943+
self.logger.warning("Overwrite existing index template (datastore.overwrite_existing_templates = true):\n%s", diff)
944+
945+
self._client.put_template("rally-metrics", new_template)
946+
916947
def index_name(self):
917948
ts = time.from_iso8601(self._race_timestamp)
918949
return "rally-metrics-%04d-%02d" % (ts.year, ts.month)

esrally/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"datastore.host",
6767
"datastore.number_of_replicas",
6868
"datastore.number_of_shards",
69+
"datastore.overwrite_existing_templates",
6970
"datastore.password",
7071
"datastore.port",
7172
"datastore.probe.cluster_version",

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ develop = [
120120
"black==24.10.0",
121121
# mypy
122122
"boto3-stubs==1.26.125",
123-
"mypy==1.10.1",
123+
"mypy==1.15.0",
124124
"types-psutil==5.9.4",
125125
"types-tabulate==0.8.9",
126126
"types-urllib3==1.26.19",

tests/metrics_test.py

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717
# pylint: disable=protected-access
18+
from __future__ import annotations
1819

1920
import datetime
2021
import json
@@ -27,17 +28,55 @@
2728
from dataclasses import dataclass
2829
from unittest import mock
2930

31+
import elastic_transport
3032
import elasticsearch.exceptions
3133
import elasticsearch.helpers
3234
import pytest
3335

3436
from esrally import config, exceptions, metrics, paths, track
3537
from esrally.metrics import GlobalStatsCalculator
3638
from esrally.track import Challenge, Operation, Task, Track
37-
from esrally.utils import opts
39+
from esrally.utils import cases, opts, pretty
40+
41+
42+
def rally_metric_template():
43+
return {
44+
"mappings": {
45+
"_source": {"enabled": True},
46+
"date_detection": False,
47+
"dynamic_templates": [
48+
{"strings": {"mapping": {"ignore_above": 8191, "type": "keyword"}, "match": "*", "match_mapping_type": "string"}}
49+
],
50+
"properties": {
51+
"@timestamp": {"format": "epoch_millis", "type": "date"},
52+
"car": {"type": "keyword"},
53+
"challenge": {"type": "keyword"},
54+
"environment": {"type": "keyword"},
55+
"job": {"type": "keyword"},
56+
"max": {"type": "float"},
57+
"mean": {"type": "float"},
58+
"median": {"type": "float"},
59+
"meta": {"properties": {"error-description": {"type": "wildcard"}}},
60+
"min": {"type": "float"},
61+
"name": {"type": "keyword"},
62+
"operation": {"type": "keyword"},
63+
"operation-type": {"type": "keyword"},
64+
"race-id": {"type": "keyword"},
65+
"race-timestamp": {"fields": {"raw": {"type": "keyword"}}, "format": "basic_date_time_no_millis", "type": "date"},
66+
"relative-time": {"type": "float"},
67+
"sample-type": {"type": "keyword"},
68+
"task": {"type": "keyword"},
69+
"track": {"type": "keyword"},
70+
"unit": {"type": "keyword"},
71+
"value": {"type": "float"},
72+
},
73+
},
74+
"settings": {"index": {"mapping": {"total_fields": {"limit": "2000"}}, "number_of_replicas": "3", "number_of_shards": "3"}},
75+
}
3876

3977

4078
class MockClientFactory:
79+
4180
def __init__(self, cfg):
4281
self._es = mock.create_autospec(metrics.EsClient)
4382

@@ -49,8 +88,8 @@ class DummyIndexTemplateProvider:
4988
def __init__(self, cfg):
5089
pass
5190

52-
def metrics_template(self):
53-
return "metrics-test-template"
91+
def metrics_template(self) -> str:
92+
return json.dumps({"index_patterns": ["rally-metrics-*"], "template": provided_metrics_template()})
5493

5594
def races_template(self):
5695
return "races-test-template"
@@ -59,6 +98,12 @@ def results_template(self):
5998
return "results-test-template"
6099

61100

101+
def provided_metrics_template() -> dict:
102+
template = rally_metric_template()
103+
template["settings"]["index"] = {"mapping.total_fields.limit": 2000, "number_of_shards": 1, "number_of_replicas": 1}
104+
return template
105+
106+
62107
class StaticClock:
63108
NOW = 1453362707
64109

@@ -438,6 +483,67 @@ def setup_method(self, method):
438483
# get hold of the mocked client...
439484
self.es_mock = self.metrics_store._client
440485
self.es_mock.exists.return_value = False
486+
self.es_mock.template_exists.return_value = False
487+
self.es_mock.get_template.return_value = mock.create_autospec(elastic_transport.ObjectApiResponse, body={"index_templates": []})
488+
self.metrics_store.logger = mock.create_autospec(logging.Logger)
489+
490+
@dataclass
491+
class OpenCase:
492+
create: bool = True
493+
template: dict | None = None
494+
overwrite_templates: str | None = None
495+
want_put_template: bool = False
496+
want_logger_call: mock._Call | None = None
497+
498+
@cases.cases(
499+
create_false=OpenCase(create=False),
500+
default=OpenCase(
501+
want_put_template=True,
502+
want_logger_call=mock.call.info("Create index template:\n%s", pretty.dump(provided_metrics_template(), pretty.Flag.FLAT_DICT)),
503+
),
504+
template_exists=OpenCase(
505+
template=rally_metric_template(),
506+
want_logger_call=mock.call.debug(
507+
"Keep existing template (datastore.overwrite_existing_templates = false):\n%s",
508+
pretty.diff(rally_metric_template(), provided_metrics_template(), pretty.Flag.FLAT_DICT),
509+
),
510+
),
511+
keep_identical_template=OpenCase(
512+
template=provided_metrics_template(), want_logger_call=mock.call.debug("Keep existing template (it is identical)")
513+
),
514+
overwrite_templates_true=OpenCase(
515+
template=rally_metric_template(),
516+
overwrite_templates="true",
517+
want_put_template=True,
518+
want_logger_call=mock.call.warning(
519+
"Overwrite existing index template (datastore.overwrite_existing_templates = true):\n%s",
520+
pretty.diff(rally_metric_template(), provided_metrics_template(), pretty.Flag.FLAT_DICT),
521+
),
522+
),
523+
overwrite_templates_false=OpenCase(
524+
template=rally_metric_template(),
525+
overwrite_templates="false",
526+
want_logger_call=mock.call.debug(
527+
"Keep existing template (datastore.overwrite_existing_templates = false):\n%s",
528+
pretty.diff(rally_metric_template(), provided_metrics_template(), pretty.Flag.FLAT_DICT),
529+
),
530+
),
531+
)
532+
def test_open(self, case: OpenCase):
533+
if case.template is not None:
534+
self.metrics_store._client.template_exists.return_value = True
535+
self.metrics_store._client.get_template.return_value.body["index_templates"] = [{"index_template": {"template": case.template}}]
536+
if case.overwrite_templates is not None:
537+
self.cfg.add(
538+
scope=config.Scope.application,
539+
section="reporting",
540+
key="datastore.overwrite_existing_templates",
541+
value=case.overwrite_templates,
542+
)
543+
self.metrics_store.open(self.RACE_ID, self.RACE_TIMESTAMP, "test", "append", "defaults", create=case.create)
544+
assert case.want_put_template == self.metrics_store._client.put_template.called
545+
if case.want_logger_call is not None:
546+
assert self.metrics_store.logger.method_calls[-1:] == [case.want_logger_call]
441547

442548
def test_put_value_without_meta_info(self):
443549
throughput = 5000

tests/types_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,5 +126,5 @@ def assert_annotations(obj, ident, *expects):
126126
class TestConfigTypeHint:
127127
def test_esrally_module_annotations(self):
128128
for module in project_root.glob_modules("esrally/**/*.py"):
129-
assert_annotations(module, "cfg", types.Config)
129+
assert_annotations(module, "cfg", types.Config, "types.Config")
130130
assert_annotations(module, "config", types.Config, Optional[types.Config], ConfigParser)

0 commit comments

Comments
 (0)