|
1 | 1 | """elasticsearch_metrics.elastic8: store events and reports in elasticsearch 8 |
2 | 2 |
|
3 | | ->>> from elasticsearch_metrics.imps import elastic8 as djelme |
4 | | ->>> class MyCountableEvent(djelme.EventLog): |
5 | | -... |
| 3 | +>>> from elasticsearch_metrics.imps.elastic8 import EventLog |
| 4 | +>>> class TheThingHappened(EventLog): |
| 5 | +... intensity: int |
| 6 | +... |
| 7 | +... class Index: # elasticsearch index settings |
| 8 | +... settings = { |
| 9 | +... "number_of_shards": 2, |
| 10 | +... "refresh_interval": "5s", |
| 11 | +... } |
6 | 12 | """ |
7 | 13 | __all__ = ( |
8 | 14 | 'TimeseriesRecord', |
9 | 15 | 'EventLog', |
10 | 16 | 'PeriodicReport', |
11 | 17 | ) |
12 | 18 | from collections import ChainMap |
| 19 | +import dataclasses |
13 | 20 | import datetime |
14 | 21 | import logging |
| 22 | +import typing |
15 | 23 |
|
16 | | -from django.apps import apps |
17 | 24 | from django.conf import settings |
18 | 25 | from django.utils import timezone |
19 | 26 | from elasticsearch8.exceptions import NotFoundError |
20 | | -from elasticsearch8_dsl import Document, connections |
21 | | -from elasticsearch8_dsl.document import IndexMeta, MetaField |
22 | | -from elasticsearch8_dsl.index import Index |
| 27 | +from elasticsearch8.dsl import Document, connections |
| 28 | +from elasticsearch8.dsl.document import MetaField |
| 29 | +from elasticsearch8.dsl.index import Index |
23 | 30 |
|
24 | 31 | from elasticsearch_metrics import signals |
25 | 32 | from elasticsearch_metrics import exceptions |
|
34 | 41 | logger = logging.getLogger(__name__) |
35 | 42 |
|
36 | 43 |
|
37 | | -class IndexInheritanceMeta(IndexMeta): |
38 | | - # override elasticsearch8.document.IndexMeta.construct_index |
39 | | - # so Index attrs are inherited |
40 | | - # and each subtype gets their own index (`opts` is never None) |
41 | | - @classmethod |
42 | | - def construct_index(cls, opts, bases): |
43 | | - parent_configs = [ |
44 | | - base._index.to_dict() for base in bases if hasattr(base, "_index") |
45 | | - ] |
46 | | - chained_opts = ( |
47 | | - ChainMap(ReadonlyAttrMap(opts), *parent_configs) |
48 | | - if opts |
49 | | - else ChainMap(*parent_configs) |
50 | | - ) |
51 | | - # since chained_opts is not None, IndexMeta.construct_index won't reuse the index from a parent type |
52 | | - return super().construct_index(chained_opts, bases) |
53 | | - |
| 44 | +# TODO: do index inheritance without metaclasses |
| 45 | +# class IndexInheritanceMeta(IndexMeta): |
| 46 | +# # override elasticsearch8.document.IndexMeta.construct_index |
| 47 | +# # so Index attrs are inherited |
| 48 | +# # and each subtype gets their own index (`opts` is never None) |
| 49 | +# @classmethod |
| 50 | +# def construct_index(cls, opts, bases): |
| 51 | +# parent_configs = [ |
| 52 | +# base._index.to_dict() for base in bases if hasattr(base, "_index") |
| 53 | +# ] |
| 54 | +# chained_opts = ( |
| 55 | +# ChainMap(ReadonlyAttrMap(opts), *parent_configs) |
| 56 | +# if opts |
| 57 | +# else ChainMap(*parent_configs) |
| 58 | +# ) |
| 59 | +# # since chained_opts is not None, IndexMeta.construct_index won't reuse the index from a parent type |
| 60 | +# return super().construct_index(chained_opts, bases) |
| 61 | +# |
54 | 62 |
|
55 | 63 | # class TimeseriesIndexMeta(IndexMeta): |
56 | 64 | # """Metaclass for the base `TimeseriesRecord` class.""" |
@@ -104,7 +112,7 @@ def construct_index(cls, opts, bases): |
104 | 112 | # # Abstract base metrics can't be instantiated and don't appear in |
105 | 113 | # # the list of metrics for an app. |
106 | 114 | # if not abstract: |
107 | | -# registry.register(app_label, new_cls) |
| 115 | +# registry.register_recordtype(app_label, new_cls) |
108 | 116 | # return new_cls |
109 | 117 | # |
110 | 118 | # # Override IndexMeta.construct_index so that |
@@ -132,46 +140,78 @@ def construct_index(cls, opts, bases): |
132 | 140 | # |
133 | 141 | # |
134 | 142 |
|
135 | | -class TimeseriesRecord(Document, metaclass=IndexInheritanceMeta): |
| 143 | +class TimeseriesRecord(Document): |
136 | 144 | """Base document |
137 | | -
|
138 | | - Example usage: |
139 | | - >>> from elasticsearch_metrics.imps.elastic8 import TimeseriesEvent |
140 | | - >>> class PageView(TimeseriesEvent): |
141 | | - ... page_route_key: str |
142 | | - ... |
143 | | - ... class Index: |
144 | | - ... settings = { |
145 | | - ... "number_of_shards": 2, |
146 | | - ... "refresh_interval": "5s", |
147 | | - ... } |
148 | 145 | """ |
149 | 146 |
|
150 | 147 | timestamp = Date(doc_values=True, required=True) |
151 | 148 |
|
152 | 149 | class Meta: |
153 | 150 | source = MetaField(enabled=False) |
| 151 | + abstract = True |
154 | 152 |
|
155 | 153 | def __init_subclass__(cls, **kwargs): |
156 | 154 | # (sidenote: simpler? way to register subclasses, compared to metaclasses) |
157 | 155 | super().__init_subclass__(**kwargs) |
158 | 156 | # TODO: register cls with metrics registry |
159 | | -# if not abstract: |
160 | | -# registry.register(app_label, cls) |
| 157 | + if not cls.get_meta_setting('abstract'): |
| 158 | + registry.register_recordtype(cls.get_app_label(), cls) |
161 | 159 |
|
162 | 160 | @classmethod |
163 | 161 | def init(cls, index=None, using=None): |
164 | 162 | """Create the index and populate the mappings in elasticsearch.""" |
165 | 163 | cls.sync_index_template(using=using) |
166 | 164 | return super().init(index=index or cls.get_timeseries_index_name(), using=using) |
167 | 165 |
|
| 166 | + @classmethod |
| 167 | + def get_timeseries_index_template(cls): |
| 168 | + """Return an `IndexTemplate <elasticsearch_dsl.IndexTemplate>` for this metric.""" |
| 169 | + return cls._index.as_template( |
| 170 | + template_name=cls._template_name, pattern=cls._template_pattern |
| 171 | + ) |
| 172 | + |
| 173 | + @classmethod |
| 174 | + def get_template_pattern(cls) -> str: |
| 175 | + return f"{cls.get_template_name()}_*" |
| 176 | + |
| 177 | + @classmethod |
| 178 | + def get_template_pattern(cls) -> str: |
| 179 | + if not template_name: |
| 180 | + metric_name = new_cls.__name__.lower() |
| 181 | + # If template_name not specified in class Meta, |
| 182 | + # compute it as <app label>_<lowercased class name> |
| 183 | + template_name = f"{app_label}_{metric_name}" |
| 184 | + |
| 185 | + new_cls._template_name = template_name |
| 186 | + |
| 187 | + @classmethod |
| 188 | + def get_app_label(cls) -> str: |
| 189 | + _app_label = self.get_meta_setting("app_label") |
| 190 | + if _app_label is None: |
| 191 | + # Look for an application configuration to attach the model to. |
| 192 | + app_config = apps.get_containing_app_config(module) |
| 193 | + if app_config is None: |
| 194 | + if not self.get_meta_setting("abstract"): |
| 195 | + raise RuntimeError( |
| 196 | + "Metric class %s.%s doesn't declare an explicit " |
| 197 | + "app_label and isn't in an application in " |
| 198 | + "INSTALLED_APPS." % (module, name) |
| 199 | + ) |
| 200 | + else: |
| 201 | + _app_label = app_config.label |
| 202 | + assert isinstance(_app_label, str) |
| 203 | + return _app_label |
| 204 | + |
| 205 | + @classmethod |
| 206 | + def get_meta_setting(cls, setting_name: str) -> typing.Any: |
| 207 | + _meta_settings_cls = getattr(cls, 'Meta', None) |
| 208 | + return getattr(_meta_settings_cls, setting_name, None) |
168 | 209 |
|
169 | 210 | ###### |
170 | 211 | @classmethod |
171 | 212 | def sync_index_template(cls, using=None): |
172 | 213 | """Sync the index template for this metric in Elasticsearch.""" |
173 | 214 | index_template = cls.get_timeseries_index_template() |
174 | | - index_template.document(cls) |
175 | 215 | signals.pre_index_template_create.send( |
176 | 216 | cls, index_template=index_template, using=using |
177 | 217 | ) |
@@ -244,13 +284,6 @@ def check_index_template(cls, using=None): |
244 | 284 | ) |
245 | 285 | return True |
246 | 286 |
|
247 | | - @classmethod |
248 | | - def get_timeseries_index_template(cls): |
249 | | - """Return an `IndexTemplate <elasticsearch_dsl.IndexTemplate>` for this metric.""" |
250 | | - return cls._index.as_template( |
251 | | - template_name=cls._template_name, pattern=cls._template_pattern |
252 | | - ) |
253 | | - |
254 | 287 | @classmethod |
255 | 288 | def get_timeseries_index_name(cls, datestamp: datetime.date | None = None) -> str: |
256 | 289 | datestamp = datestamp or timezone.now().date() |
@@ -306,3 +339,43 @@ class PeriodicReport(TimeseriesRecord): |
306 | 339 | timestamp: datetime.datetime |
307 | 340 | report_label: str |
308 | 341 | report_coverage: ... # timespan |
| 342 | + |
| 343 | + |
| 344 | +@dataclasses.dataclass |
| 345 | +class DjelmeElastic8Imp(ProtoTimeseriesImp): |
| 346 | + """DjelmeElastic8Imp: the elastic8 implementation of djelme (for use by generic djelme code)""" |
| 347 | + |
| 348 | + imp_name: str |
| 349 | + imp_config: dict[str, str] |
| 350 | + namespace_prefix: str = "" |
| 351 | + |
| 352 | + @property |
| 353 | + def elastic8_client(self): |
| 354 | + # assumes `configure` was already called |
| 355 | + return connections.get_connection(self.imp_name) |
| 356 | + |
| 357 | + def configure(self) -> None: |
| 358 | + connections.configure(**{self.imp_name: self.imp_config}) |
| 359 | + |
| 360 | + def setup_timeseries_indexes(self) -> None: |
| 361 | + for _metric_type in self._each_metric_type(): |
| 362 | + # TODO: logger.info |
| 363 | + _metric_type.sync_index_template(using=self.elastic6_client) |
| 364 | + |
| 365 | + def teardown_timeseries_indexes(self) -> None: |
| 366 | + for _metric_type in self._each_metric_type(): |
| 367 | + _indexname_wildcard = _metric_type._template_pattern |
| 368 | + _templatename = _metric_type._template_name |
| 369 | + self.elastic6_client.indices.delete(index=_indexname_wildcard) |
| 370 | + try: |
| 371 | + self.elastic6_client.indices.delete_template(_templatename) |
| 372 | + except NotFoundError: |
| 373 | + pass |
| 374 | + |
| 375 | + def _each_metric_type(self) -> Iterator[type[Metric]]: |
| 376 | + for _metric in registry.each_recordtype(imp_name=self.imp_name): |
| 377 | + assert issubclass(_metric, Metric) |
| 378 | + yield _metric |
| 379 | + |
| 380 | + |
| 381 | +djelme_imp_from_config = DjelmeElastic8Imp # for ProtoDjelmetricsImpModule |
0 commit comments