feat(homeassistant): its page offers the metrics scrape under the same sign-in, so one user covers everything (#6744)
A Prometheus target's token_from names the plugin that supplies its token at each scrape. Read its metrics makes the target; Use this sign-in moves one at that address across, keeping its name, filters and history. A refusal stops the connection, so one Try again or sign-in starts everything, and the sign-in is only sent to its own address. A 404 says how to turn on Home Assistant's Prometheus integration, as Steward doesn't change Home Assistant. Refused also joins the volatile columns a backup check ignores. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,14 +48,33 @@ def is_due(target, now: datetime) -> bool:
|
||||
return elapsed >= target.interval_seconds - TICK_SECONDS / 2
|
||||
|
||||
|
||||
def _token_source(name: str):
|
||||
from steward.core.plugin_manager import loaded_plugin_submodule
|
||||
return loaded_plugin_submodule(name, "metrics")
|
||||
|
||||
|
||||
async def target_token(app: "Quart", target, url: str) -> str:
|
||||
"""The token to ask `url` with: the target's own, or one from the plugin whose
|
||||
sign-in it uses. Raises TokenUnavailable when that plugin has none to give."""
|
||||
from steward.core.crypto import decrypt_secret
|
||||
from steward.core.prometheus import TokenUnavailable
|
||||
|
||||
if not target.token_from:
|
||||
return decrypt_secret(target.token, context=f"prometheus target {target.name} token")
|
||||
source = _token_source(target.token_from)
|
||||
if source is None:
|
||||
raise TokenUnavailable(f"Its token comes from the {target.token_from} plugin, which isn't turned on.")
|
||||
return await source.scrape_token(app, url)
|
||||
|
||||
|
||||
async def scrape_due(app: "Quart", scrape=None) -> None:
|
||||
"""Scrape every due target at once, then write each one's outcome to its
|
||||
row and its series to the metric tables. `scrape` stands in for
|
||||
scraper.scrape in tests."""
|
||||
from steward.core.crypto import decrypt_secret
|
||||
from steward.core.prometheus import TokenUnavailable
|
||||
|
||||
from .models import PrometheusTarget
|
||||
from .scraper import ScrapeError
|
||||
from .scraper import ScrapeError, TokenRefused
|
||||
from .scraper import scrape as fetch
|
||||
|
||||
scrape = scrape or fetch
|
||||
@@ -68,11 +87,15 @@ async def scrape_due(app: "Quart", scrape=None) -> None:
|
||||
|
||||
async def one(t):
|
||||
started = time.monotonic()
|
||||
token = decrypt_secret(t.token, context=f"prometheus target {t.name} token")
|
||||
|
||||
async def fetched():
|
||||
return await scrape(t.url, await target_token(app, t, t.url), t.verify_tls)
|
||||
|
||||
try:
|
||||
families = await asyncio.wait_for(scrape(t.url, token, t.verify_tls),
|
||||
SCRAPE_DEADLINE_SECONDS)
|
||||
families = await asyncio.wait_for(fetched(), SCRAPE_DEADLINE_SECONDS)
|
||||
return families, (time.monotonic() - started) * 1000
|
||||
except TokenUnavailable as exc:
|
||||
return ScrapeError(str(exc)), (time.monotonic() - started) * 1000
|
||||
except TimeoutError:
|
||||
return ScrapeError(f"Gave up after {SCRAPE_DEADLINE_SECONDS} s."), \
|
||||
(time.monotonic() - started) * 1000
|
||||
@@ -81,6 +104,11 @@ async def scrape_due(app: "Quart", scrape=None) -> None:
|
||||
|
||||
results = await asyncio.gather(*(one(t) for t in targets))
|
||||
await record(app, targets, results, datetime.now(timezone.utc))
|
||||
# A refused sign-in is the lending plugin's to stop, so one Try again or sign-in
|
||||
# there starts everything that uses it.
|
||||
for t, (result, _) in zip(targets, results):
|
||||
if t.token_from and isinstance(result, TokenRefused) and (source := _token_source(t.token_from)):
|
||||
await source.token_refused(app)
|
||||
|
||||
|
||||
async def forget_gone(session, target: str, kept: set[tuple[str, str]]) -> None:
|
||||
@@ -132,7 +160,7 @@ async def record(app: "Quart", targets, results, now: datetime) -> None:
|
||||
if not isinstance(result, ScrapeError):
|
||||
logger.warning("Prometheus scrape of %s failed: %r", target.name, result)
|
||||
row.last_error = str(result) or "The scrape failed."
|
||||
row.refused = isinstance(result, TokenRefused)
|
||||
row.refused = isinstance(result, TokenRefused) and not row.token_from
|
||||
batch.add(source_module=SOURCE, resource_name=row.name, metric_name=IS_UP,
|
||||
value=0.0, recorded_at=now)
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user