1
1
import asyncio
2
+ import operator
2
3
import signal
3
4
import sys
4
5
from abc import ABC , abstractmethod
5
- from datetime import timedelta
6
- from typing import Any , Coroutine , Type
6
+ from collections import defaultdict
7
+ from datetime import datetime , timedelta
8
+ from decimal import Decimal
9
+ from typing import Any , Callable , Coroutine , Type
7
10
8
11
import pycron # type: ignore[import-untyped]
9
12
import quattro
34
37
from .main import SilverbackBot , TaskData
35
38
from .recorder import BaseRecorder , TaskResult
36
39
from .state import Datastore , StateSnapshot
37
- from .types import TaskType , utc_now
40
+ from .types import Datapoint , ScalarDatapoint , ScalarType , TaskType , utc_now
38
41
from .utils import async_wrap_iter , clean_hexbytes_dict , decode_topics_from_string
39
42
40
43
if sys .version_info < (3 , 11 ):
@@ -57,6 +60,9 @@ def __init__(
57
60
# TODO: Allow configuring datastore class
58
61
self .datastore = Datastore ()
59
62
self .recorder = recorder
63
+ self .metric_handlers : dict [str , list [Callable [[Datapoint , datetime ], Coroutine ]]] = (
64
+ defaultdict (list )
65
+ )
60
66
61
67
self .max_exceptions = max_exceptions
62
68
self .exceptions = 0
@@ -104,6 +110,11 @@ async def run_task(self, task_data: TaskData, *args, raise_on_error: bool = Fals
104
110
): # Display metrics in logs to help debug
105
111
logger .info (f"{ task_data .name } - Metrics collected\n { metrics_str } " )
106
112
113
+ # Trigger checks for metric values
114
+ for metric_name , datapoint in result .metrics .items ():
115
+ for handler in self .metric_handlers [metric_name ]:
116
+ self ._runtime_task_group .create_task (handler (datapoint , result .completed ))
117
+
107
118
if self .recorder : # Recorder configured to record
108
119
await self .recorder .add_result (result )
109
120
@@ -156,6 +167,25 @@ async def _cron_tasks(self, cron_tasks: list[TaskData]):
156
167
# NOTE: Run this every minute (just in case of an unhandled shutdown)
157
168
self ._runtime_task_group .create_task (self ._checkpoint ())
158
169
170
+ async def _metric_task (self , task_data : TaskData ) -> None :
171
+ metric_name = task_data .labels ["metric" ]
172
+ value_thresholds = {
173
+ op : Decimal (val ) # NOTE: Decimal is most flexible at handling strings
174
+ for lbl , val in task_data .labels .items ()
175
+ if lbl .startswith ("value:" ) and (op := getattr (operator , lbl .lstrip ("value:" ), None ))
176
+ }
177
+
178
+ def exceeds_value_threshold (data : ScalarType ) -> bool :
179
+ return all (op (Decimal (data ), value ) for op , value in value_thresholds .items ())
180
+
181
+ async def check_value (datapoint : Datapoint , updated : datetime ):
182
+ if isinstance (datapoint , ScalarDatapoint ) and exceeds_value_threshold (datapoint .data ):
183
+ self ._runtime_task_group .create_task (self .run_task (task_data , datapoint .data ))
184
+
185
+ self .metric_handlers [metric_name ].append (check_value )
186
+
187
+ # TODO: Support rate threshold checks?
188
+
159
189
@abstractmethod
160
190
async def _block_task (self , task_data : TaskData ) -> None :
161
191
"""
@@ -260,10 +290,15 @@ async def startup(self) -> list[Coroutine]:
260
290
TaskType .SYSTEM_USER_TASKDATA , TaskType .EVENT_LOG
261
291
)
262
292
293
+ metric_value_tasks_taskdata = await self .run_system_task (
294
+ TaskType .SYSTEM_USER_TASKDATA , TaskType .METRIC_VALUE
295
+ )
296
+
263
297
if (
264
298
len (cron_tasks_taskdata )
265
299
== len (new_block_tasks_taskdata )
266
300
== len (event_log_tasks_taskdata )
301
+ # NOTE: Skip metric value tasks, because they require other tasks to function
267
302
== 0
268
303
):
269
304
raise NoTasksAvailableError ()
@@ -272,6 +307,7 @@ async def startup(self) -> list[Coroutine]:
272
307
self ._cron_tasks (cron_tasks_taskdata ),
273
308
* map (self ._block_task , new_block_tasks_taskdata ),
274
309
* map (self ._event_task , event_log_tasks_taskdata ),
310
+ * map (self ._metric_task , metric_value_tasks_taskdata ),
275
311
]
276
312
277
313
def _cleanup_tasks (self ) -> list [Coroutine ]:
0 commit comments