LSST Applications g0603fd7c41+501e3db9f9,g0aad566f14+23d8574c86,g0dd44d6229+a1a4c8b791,g2079a07aa2+86d27d4dc4,g2305ad1205+a62672bbc1,g2bbee38e9b+047b288a59,g337abbeb29+047b288a59,g33d1c0ed96+047b288a59,g3a166c0a6a+047b288a59,g3d1719c13e+23d8574c86,g487adcacf7+cb7fd919b2,g4be5004598+23d8574c86,g50ff169b8f+96c6868917,g52b1c1532d+585e252eca,g591dd9f2cf+4a9e435310,g63cd9335cc+585e252eca,g858d7b2824+23d8574c86,g88963caddf+0cb8e002cc,g99cad8db69+43388bcaec,g9ddcbc5298+9a081db1e4,ga1e77700b3+a912195c07,gae0086650b+585e252eca,gb0e22166c9+60f28cb32d,gb2522980b2+793639e996,gb3a676b8dc+b4feba26a1,gb4b16eec92+63f8520565,gba4ed39666+c2a2e4ac27,gbb8dafda3b+a5d255a82e,gc120e1dc64+d820f8acdb,gc28159a63d+047b288a59,gc3e9b769f7+f4f1cc6b50,gcf0d15dbbd+a1a4c8b791,gdaeeff99f8+f9a426f77a,gdb0af172c8+b6d5496702,ge79ae78c31+047b288a59,w.2024.19
LSST Data Management Base Package
Loading...
Searching...
No Matches
monitor.py
Go to the documentation of this file.
1# This file is part of dax_apdb.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22from __future__ import annotations
23
24__all__ = ["MonAgent", "MonService", "LoggingMonHandler"]
25
26import contextlib
27import json
28import logging
29import time
30from abc import ABC, abstractmethod
31from collections.abc import Iterable, Iterator, Mapping
32from typing import TYPE_CHECKING, Any
33
34from lsst.utils.classes import Singleton
35
36if TYPE_CHECKING:
37 from contextlib import AbstractContextManager
38
39_TagsType = Mapping[str, str | int]
40
41
42class MonHandler(ABC):
43 """Interface for handlers of the monitoring records.
44
45 Handlers are responsible for delivering monitoring records to their final
46 destination, for example log file or time-series database.
47 """
48
49 @abstractmethod
50 def handle(
51 self, name: str, timestamp: float, tags: _TagsType, values: Mapping[str, Any], agent_name: str
52 ) -> None:
53 """Handle one monitoring record.
54
55 Parameters
56 ----------
57 name : `str`
58 Record name, arbitrary string.
59 timestamp : `str`
60 Time in seconds since UNIX epoch when record originated.
61 tags : `~collections.abc.Mapping` [`str`, `str` or `int`]
62 Tags associated with the record, may be empty.
63 values : `~collections.abc.Mapping` [`str`, `Any`]
64 Values associated with the record, usually never empty.
65 agent_name `str`
66 Name of a client agent that produced this record.
67 """
68 raise NotImplementedError()
69
70
72 """Client-side interface for adding monitoring records to the monitoring
73 service.
74
75 Parameters
76 ----------
77 name : `str`
78 Client agent name, this is used for filtering of the records by the
79 service and ia also passed to monitoring handler as ``agent_name``.
80 """
81
82 def __init__(self, name: str = ""):
83 self._name = name
85
87 self,
88 name: str,
89 *,
90 values: Mapping[str, Any],
91 tags: Mapping[str, str | int] | None = None,
92 timestamp: float | None = None,
93 ) -> None:
94 """Send one record to monitoring service.
95
96 Parameters
97 ----------
98 name : `str`
99 Record name, arbitrary string.
100 values : `~collections.abc.Mapping` [`str`, `Any`]
101 Values associated with the record, usually never empty.
102 tags : `~collections.abc.Mapping` [`str`, `str` or `int`]
103 Tags associated with the record, may be empty.
104 timestamp : `str`
105 Time in seconds since UNIX epoch when record originated.
106 """
107 self._service._add_record(
108 agent_name=self._name,
109 record_name=name,
110 tags=tags,
111 values=values,
112 timestamp=timestamp,
113 )
114
115 def context_tags(self, tags: _TagsType) -> AbstractContextManager[None]:
116 """Context manager that adds a set of tags to all records created
117 inside the context.
118
119 Parameters
120 ----------
121 tags : `~collections.abc.Mapping` [`str`, `str` or `int`]
122 Tags associated with the records.
123
124 Notes
125 -----
126 All calls to `add_record` that happen inside the corresponding context
127 will add tags specified in this call. Tags specified in `add_record`
128 will override matching tag names that are passed to this method. On
129 exit from context a previous tag context is restored (which may be
130 empty).
131 """
132 return self._service.context_tags(tags)
133
134
136 """Filter for the names associated with client agents.
137
138 Parameters
139 ----------
140 rule : `str`
141 String specifying filtering rule for a single name, or catch-all rule.
142 The rule consist of the agent name prefixed by minus or optional plus
143 sign. Catch-all rule uses name "any". If the rule starts with minus
144 sign then matching agent will be rejected. Otherwise matching agent
145 is accepted.
146 """
147
148 def __init__(self, rule: str):
149 self._accept = True
150 if rule.startswith("-"):
151 self._accept = False
152 rule = rule[1:]
153 elif rule.startswith("+"):
154 rule = rule[1:]
155 self.agent_name = "" if rule == "any" else rule
156
157 def is_match_all(self) -> bool:
158 """Return `True` if this rule is a catch-all rule.
159
160 Returns
161 -------
162 is_match_all : `bool`
163 `True` if rule name is `-any`, `+any`, or `any`.
164 """
165 return not self.agent_name
166
167 def accept(self, agent_name: str) -> bool | None:
168 """Return filtering decision for specified agent name.
169
170 Parameters
171 ----------
172 agent_name : `str`
173 Name of the clent agent that produces monitoring record.
174
175 Returns
176 -------
177 decision : `bool` or `None`
178 `True` if the agent is accepted, `False` if agent is rejected.
179 `None` is returned if this rule does not match agent name and
180 decision should be made by the next rule.
181 """
182 if not self.agent_name or agent_name == self.agent_name:
183 return self._accept
184 return None
185
186
187class MonService(metaclass=Singleton):
188 """Class implementing monitoring service functionality.
189
190 Notes
191 -----
192 This is a singleton class which serves all client agents in an application.
193 It accepts records from agents, filters it based on a set of configured
194 rules and forwards them to one or more configured handlers. By default
195 there are no handlers defined which means that all records are discarded.
196 Default set of filtering rules is empty which accepts all agent names.
197
198 To produce a useful output from this service one has to add at least one
199 handler using `add_handler` method (e.g. `LoggingMonHandler` instance).
200 The `set_filters` methods can be used to specify the set of filtering
201 rules.
202 """
203
204 _handlers: list[MonHandler] = []
205 """List of active handlers."""
206
207 _context_tags: _TagsType | None = None
208 """Current tag context, these tags are added to each new record."""
209
210 _filters: list[MonFilter] = []
211 """Sequence of filters for agent names."""
212
213 def set_filters(self, rules: Iterable[str]) -> None:
214 """Define a sequence of rules for filtering of the agent names.
215
216 Parameters
217 ----------
218 rules : `~collections.abc.Iterable` [`str`]
219 Ordered collection of rules. Each string specifies filtering rule
220 for a single name, or catch-all rule. The rule consist of the
221 agent name prefixed by minus or optional plus sign. Catch-all rule
222 uses name "any". If the rule starts with minus sign then matching
223 agent will be rejected. Otherwise matching agent is accepted.
224
225 Notes
226 -----
227 The catch-all rule (`-any`, `+any`, or `any`) can be specified in any
228 location in the sequence but it is always applied last. E.g.
229 `["-any", "+agent1"]` behaves the same as `["+agent1", "-any"]`.
230 If the set of rues does not include catch-all rule, filtering behaves
231 as if it is added implicitly as `+any`.
232
233 Filtering code evaluates each rule in order. First rule that matches
234 the agent name wins. Agent names are matched literally, wildcards are
235 not supported and there are no parent/child relations between agent
236 names (e.g `lsst.dax.apdb` and `lsst.dax.apdb.sql` are treated as
237 independent names).
238 """
239 match_all: MonFilter | None = None
241 for rule in rules:
242 mon_filter = MonFilter(rule)
243 if mon_filter.is_match_all():
244 match_all = mon_filter
245 else:
246 self._filters_filters.append(mon_filter)
247 if match_all:
248 self._filters_filters.append(match_all)
249
251 self,
252 *,
253 agent_name: str,
254 record_name: str,
255 values: Mapping[str, Any],
256 tags: Mapping[str, str | int] | None = None,
257 timestamp: float | None = None,
258 ) -> None:
259 """Add one monitoring record, this method is for use by agents only."""
260 if self._handlers:
261 accept: bool | None = None
262 # Check every filter, accept if none makes any decision.
263 for filter in self._filters_filters:
264 accept = filter.accept(agent_name)
265 if accept is False:
266 return
267 if accept is True:
268 break
269 if timestamp is None:
270 timestamp = time.time()
271 if tags is None:
272 tags = self._context_tags_context_tags or {}
273 else:
275 all_tags = dict(self._context_tags_context_tags)
276 all_tags.update(tags)
277 tags = all_tags
278 for handler in self._handlers:
279 handler.handle(record_name, timestamp, tags, values, agent_name)
280
281 @property
282 def handlers(self) -> Iterable[MonHandler]:
283 """Set of handlers defined currently."""
284 return self._handlers
285
286 def add_handler(self, handler: MonHandler) -> None:
287 """Add one monitoring handler.
288
289 Parameters
290 ----------
291 handler : `MonHandler`
292 Handler instance.
293 """
294 if handler not in self._handlers:
295 self._handlers.append(handler)
296
297 def remove_handler(self, handler: MonHandler) -> None:
298 """Add one monitoring handler.
299
300 Parameters
301 ----------
302 handler : `MonHandler`
303 Handler instance.
304 """
305 if handler in self._handlers:
306 self._handlers.remove(handler)
307
308 def _add_context_tags(self, tags: _TagsType) -> _TagsType | None:
309 """Extend the tag context with new tags, overriding any tags that may
310 already exist in a current context.
311 """
312 old_tags = self._context_tags_context_tags
313 if not self._context_tags_context_tags:
315 else:
316 all_tags = dict(self._context_tags_context_tags)
317 all_tags.update(tags)
318 self._context_tags_context_tags = all_tags
319 return old_tags
320
321 @contextlib.contextmanager
322 def context_tags(self, tags: _TagsType) -> Iterator[None]:
323 """Context manager that adds a set of tags to all records created
324 inside the context.
325
326 Typically clients will be using `MonAgent.context_tags`, which forwards
327 to this method.
328 """
329 old_context = self._add_context_tags(tags)
330 try:
331 yield
332 finally:
333 # Restore old context.
334 self._context_tags_context_tags = old_context
335
336
338 """Implementation of the monitoring handler which dumps records formatted
339 as JSON objects to `logging`.
340
341 Parameters
342 ----------
343 logger_name : `str`
344 Name of the `logging` logger to use for output.
345 log_level : `int`, optional
346 Logging level to use for output, default is `INFO`
347
348 Notes
349 -----
350 The attributes of the formatted JSON object correspond to the parameters
351 of `handle` method, except for `agent_name` which is mapped to `source`.
352 The `tags` and `values` become JSON sub-objects with corresponding keys.
353 """
354
355 def __init__(self, logger_name: str, log_level: int = logging.INFO):
356 self._logger = logging.getLogger(logger_name)
357 self._level = log_level
358
360 self, name: str, timestamp: float, tags: _TagsType, values: Mapping[str, Any], agent_name: str
361 ) -> None:
362 # Docstring is inherited from base class.
363 record = {
364 "name": name,
365 "timestamp": timestamp,
366 "tags": tags,
367 "values": values,
368 "source": agent_name,
369 }
370 msg = json.dumps(record)
371 self._logger.log(self._level, msg)
None handle(self, str name, float timestamp, _TagsType tags, Mapping[str, Any] values, str agent_name)
Definition monitor.py:361
__init__(self, str logger_name, int log_level=logging.INFO)
Definition monitor.py:355
AbstractContextManager[None] context_tags(self, _TagsType tags)
Definition monitor.py:115
None add_record(self, str name, *Mapping[str, Any] values, Mapping[str, str|int]|None tags=None, float|None timestamp=None)
Definition monitor.py:93
__init__(self, str name="")
Definition monitor.py:82
bool|None accept(self, str agent_name)
Definition monitor.py:167
None handle(self, str name, float timestamp, _TagsType tags, Mapping[str, Any] values, str agent_name)
Definition monitor.py:52
Iterator[None] context_tags(self, _TagsType tags)
Definition monitor.py:322
None remove_handler(self, MonHandler handler)
Definition monitor.py:297
_TagsType|None _add_context_tags(self, _TagsType tags)
Definition monitor.py:308
None add_handler(self, MonHandler handler)
Definition monitor.py:286
None _add_record(self, *str agent_name, str record_name, Mapping[str, Any] values, Mapping[str, str|int]|None tags=None, float|None timestamp=None)
Definition monitor.py:258
Iterable[MonHandler] handlers(self)
Definition monitor.py:282
None set_filters(self, Iterable[str] rules)
Definition monitor.py:213