Source code for zabgrab.services.metadata

"""Metadata saved with fetched data: enough to understand and repeat the fetch.

Files written by zabgrab store it as JSON (an HDF5 dataset, or ``#`` comment lines
in delimited files). ``update`` relies on its ``query`` and ``items``.
"""

from __future__ import annotations

import copy
from collections.abc import Mapping, Sequence
from datetime import UTC, datetime
from typing import Any

from .. import __version__
from ..model.choice import Choice
from ..model.config import Template
from ..model.query import Query
from ..model.series import FORMAT_VERSION, FetchResult, Series, Skipped
from ..model.server import Server
from ..model.zabbix import Item
from .dataset import Dataset


[docs] def build_metadata( query: Query, result: FetchResult, *, template: Template | None, command: Sequence[str], created: datetime, ) -> dict[str, Any]: """Describe a fetch. Never includes credentials. Parameters ---------- query : Query What was fetched, and where each setting came from. result : FetchResult The data, and items skipped or failed. template : Template or None The template used, copied in so the fetch can be understood (and updated with ``--rematch``) without the config. command : sequence of str The command line. created : datetime When the fetch was made. Returns ------- dict The metadata, ready for JSON. """ servers: dict[str, Server] = {} entries: list[Series | Skipped] = [*result.series, *result.skipped, *result.failed] for entry in entries: servers.setdefault(entry.server.url, entry.server) return { "zabgrab": {"version": __version__, "format_version": FORMAT_VERSION}, "created": created.isoformat(), "command": list(command), "query": { # A list, from when fetches could use several templates. "templates": [query.template] if query.template else [], "custom": query.custom, "from": query.from_text, "to": query.to_text, "start": query.start.isoformat(), "end": query.end.isoformat(), "timezone": query.timezone, "resolution": str(query.resolution), "agg": [str(agg) for agg in query.agg], "sources": dict(query.sources), "requests": [ { "label": request.label, "server": _server(query.server), "items": [ spec.model_dump(mode="json", exclude_none=True) for spec in request.items ], } for request in query.requests ], }, # A table of templates, from when fetches could use several. "templates": { query.template: template.model_dump( mode="json", by_alias=True, exclude_none=True ) } if query.template and template is not None else {}, "servers": [{"name": s.name, "url": url} for url, s in servers.items()], "items": [_series(series) for series in result.series], "skipped": [_skipped(s) for s in result.skipped], "failed": [_skipped(s) for s in result.failed], }
def _server(server: str | Choice[str] | None) -> Any: """Convert the query's server for JSON (a choice as a table).""" if isinstance(server, Choice): return server.model_dump(mode="json") return server def _series(series: Series) -> dict[str, Any]: """Describe a fetched series: its item and what was stored.""" return { **_item(series.server.url, series.item), "requests": list(series.requests), "columns": list(series.columns), "points": series.points, } def _item(url: str, item: Item) -> dict[str, Any]: """Describe an item and its server.""" return { "server": url, "itemid": item.itemid, "host": item.host, "host_name": item.host_name, "name": item.name, "key": item.key, "units": item.units, "value_type": item.value_type.label, "tags": [{"tag": tag, "value": value} for tag, value in item.tags], } def _skipped(skipped: Skipped) -> dict[str, Any]: """Describe an item that wasn't fetched, and why.""" return {**_item(skipped.server.url, skipped.item), "reason": skipped.reason}
[docs] def export_metadata( dataset: Dataset, *, source: str, filters: Mapping[str, Any], command: Sequence[str], created: datetime, ) -> dict[str, Any]: """Describe data exported from a file. The last group's query is kept, so the export can be updated like the original, with the merged items and a record of how it was exported. Parameters ---------- dataset : Dataset The merged data; it must cover some time. source : str The file it came from. filters : mapping The selection options used. command : sequence of str The command line. created : datetime When the export was made. Returns ------- dict The metadata, ready for JSON. """ metadata = copy.deepcopy(dataset.last_metadata) metadata["zabgrab"] = {"version": __version__, "format_version": FORMAT_VERSION} metadata["created"] = created.isoformat() metadata["command"] = list(command) query = metadata.setdefault("query", {}) query["start"] = _iso(dataset.coverage[0][0]) query["end"] = _iso(dataset.coverage[-1][1]) metadata["items"] = [_series(series) for series in dataset.series] metadata["skipped"] = [] metadata["failed"] = [] metadata["export"] = { "source": source, "groups": dataset.groups, "filters": dict(filters), "coverage": [[_iso(a), _iso(b)] for a, b in dataset.coverage], } return metadata
def _iso(seconds: float) -> str: """Format POSIX seconds as ISO 8601 in UTC.""" return datetime.fromtimestamp(seconds, UTC).isoformat()