Skip to content

Commit d8254c4

Browse files
committed
wipwip
1 parent c4d3b62 commit d8254c4

19 files changed

Lines changed: 254 additions & 142 deletions

elasticsearch_metrics/apps.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from collections.abc import Iterator
2-
import importlib
32

43
from django.apps import AppConfig
54
from django.conf import settings
@@ -21,6 +20,9 @@ def ready(self) -> None:
2120
autodiscover_modules("metrics")
2221

2322

23+
###
24+
# accessing django settings
25+
2426
def each_timeseries_imp_config() -> Iterator[tuple[str, str, dict[str, str]]]:
2527
for _imp_name in getattr(settings, _IMPCONFIG_SETTINGS_KEY, ()):
2628
(_imp_module_path, _imp_config) = get_timeseries_imp_config(_imp_name)
@@ -29,9 +31,11 @@ def each_timeseries_imp_config() -> Iterator[tuple[str, str, dict[str, str]]]:
2931

3032
def get_timeseries_imp_config(imp_name: str) -> tuple[str, dict[str, str]]:
3133
try:
32-
_imp_module_path, _imp_config = getattr(settings, _IMPCONFIG_SETTINGS_KEY)
34+
_imps_setting = getattr(settings, _IMPCONFIG_SETTINGS_KEY)
3335
except AttributeError as _error:
3436
raise ValueError(f"no `settings.{_IMPCONFIG_SETTINGS_KEY}` found") from _error
37+
(_imp_module_path, _imp_config) = _imps_setting.get(imp_name, ('', {}))
38+
# TODO: better errors
3539
assert isinstance(_imp_module_path, str)
3640
assert isinstance(_imp_config, dict)
3741
assert all(

elasticsearch_metrics/djelmetrics_imps.py

Lines changed: 0 additions & 46 deletions
This file was deleted.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
__all__ = (
2+
'elastic6',
3+
'elastic8',
4+
)

elasticsearch_metrics/imps/elastic6.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def __new__(mcls, name, bases, attrs): # noqa: B902
8585
# Abstract base metrics can't be instantiated and don't appear in
8686
# the list of metrics for an app.
8787
if not abstract:
88-
registry.register(app_label, new_cls)
88+
registry.register_recordtype(app_label, new_cls)
8989
return new_cls
9090

9191
# Override IndexMeta.construct_index so that
@@ -224,7 +224,7 @@ def get_timeseries_index_template(cls):
224224
)
225225

226226
@classmethod
227-
def get_index_name(cls, date=None):
227+
def get_index_name(cls, date=None) -> str:
228228
date = date or timezone.now().date()
229229
dateformat = getattr(
230230
settings, "ELASTICSEARCH_METRICS_DATE_FORMAT", DEFAULT_DATE_FORMAT
@@ -282,6 +282,7 @@ class DjelmeElastic6Imp(ProtoTimeseriesImp):
282282

283283
imp_name: str
284284
imp_config: dict[str, str]
285+
namespace_prefix: str = ""
285286

286287
@property
287288
def elastic6_client(self):
@@ -307,7 +308,8 @@ def teardown_timeseries_indexes(self) -> None:
307308
pass
308309

309310
def _each_metric_type(self) -> Iterator[type[Metric]]:
310-
for _metric in registry.get_metrics(imp_name=self.imp_name):
311+
for _metric in registry.each_recordtype(imp_name=self.imp_name):
312+
assert issubclass(_metric, Metric)
311313
yield _metric
312314

313315

elasticsearch_metrics/imps/elastic8.py

Lines changed: 120 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,32 @@
11
"""elasticsearch_metrics.elastic8: store events and reports in elasticsearch 8
22
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+
... }
612
"""
713
__all__ = (
814
'TimeseriesRecord',
915
'EventLog',
1016
'PeriodicReport',
1117
)
1218
from collections import ChainMap
19+
import dataclasses
1320
import datetime
1421
import logging
22+
import typing
1523

16-
from django.apps import apps
1724
from django.conf import settings
1825
from django.utils import timezone
1926
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
2330

2431
from elasticsearch_metrics import signals
2532
from elasticsearch_metrics import exceptions
@@ -34,23 +41,24 @@
3441
logger = logging.getLogger(__name__)
3542

3643

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+
#
5462

5563
# class TimeseriesIndexMeta(IndexMeta):
5664
# """Metaclass for the base `TimeseriesRecord` class."""
@@ -104,7 +112,7 @@ def construct_index(cls, opts, bases):
104112
# # Abstract base metrics can't be instantiated and don't appear in
105113
# # the list of metrics for an app.
106114
# if not abstract:
107-
# registry.register(app_label, new_cls)
115+
# registry.register_recordtype(app_label, new_cls)
108116
# return new_cls
109117
#
110118
# # Override IndexMeta.construct_index so that
@@ -132,46 +140,78 @@ def construct_index(cls, opts, bases):
132140
#
133141
#
134142

135-
class TimeseriesRecord(Document, metaclass=IndexInheritanceMeta):
143+
class TimeseriesRecord(Document):
136144
"""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-
... }
148145
"""
149146

150147
timestamp = Date(doc_values=True, required=True)
151148

152149
class Meta:
153150
source = MetaField(enabled=False)
151+
abstract = True
154152

155153
def __init_subclass__(cls, **kwargs):
156154
# (sidenote: simpler? way to register subclasses, compared to metaclasses)
157155
super().__init_subclass__(**kwargs)
158156
# 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)
161159

162160
@classmethod
163161
def init(cls, index=None, using=None):
164162
"""Create the index and populate the mappings in elasticsearch."""
165163
cls.sync_index_template(using=using)
166164
return super().init(index=index or cls.get_timeseries_index_name(), using=using)
167165

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)
168209

169210
######
170211
@classmethod
171212
def sync_index_template(cls, using=None):
172213
"""Sync the index template for this metric in Elasticsearch."""
173214
index_template = cls.get_timeseries_index_template()
174-
index_template.document(cls)
175215
signals.pre_index_template_create.send(
176216
cls, index_template=index_template, using=using
177217
)
@@ -244,13 +284,6 @@ def check_index_template(cls, using=None):
244284
)
245285
return True
246286

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-
254287
@classmethod
255288
def get_timeseries_index_name(cls, datestamp: datetime.date | None = None) -> str:
256289
datestamp = datestamp or timezone.now().date()
@@ -306,3 +339,43 @@ class PeriodicReport(TimeseriesRecord):
306339
timestamp: datetime.datetime
307340
report_label: str
308341
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

elasticsearch_metrics/management/commands/check_metrics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def handle(self, *args, **options):
3939
out_of_sync_count = 0
4040
self.stdout.write("Checking for outdated index templates...")
4141
for app_label in app_labels:
42-
for metric in registry.get_metrics(app_label=app_label):
42+
for metric in registry.each_recordtype(app_label=app_label):
4343
try:
4444
metric.check_index_template(using=connection)
4545
except (

elasticsearch_metrics/management/commands/show_metrics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def handle(self, *args, **options):
2424
self.stdout.write(
2525
"Metrics for '{}':".format(app_label), style.MIGRATE_HEADING
2626
)
27-
for metric in registry.get_metrics(app_label=app_label):
27+
for metric in registry.each_recordtype(app_label=app_label):
2828
metric_name = style.METRIC(metric.__name__)
2929
template_name = metric._template_name
3030
template_pattern = style.ES_TEMPLATE(metric._template_pattern)

elasticsearch_metrics/management/commands/sync_metrics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def handle(self, *args, **options):
3838
self.stdout.write(
3939
"Syncing metrics for app: '{}'".format(app_label), style.MIGRATE_HEADING
4040
)
41-
for metric in registry.get_metrics(app_label=app_label):
41+
for metric in registry.each_recordtype(app_label=app_label):
4242
metric_name = style.METRIC(metric.__name__)
4343
template_name = metric._template_name
4444
template_pattern = style.ES_TEMPLATE(metric._template_pattern)

elasticsearch_metrics/protocols.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ class ProtoTimeseriesImpModule(typing.Protocol):
2828
def djelme_imp_from_config(
2929
imp_name: str,
3030
imp_config: dict[str, str],
31+
namespace_prefix: str = "",
3132
) -> ProtoTimeseriesImp: ...

0 commit comments

Comments
 (0)