mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-11-01 03:10:33 +03:00
Add the install and hardware analytics collectors
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""One collector per report section."""
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Helpers the section collectors share."""
|
||||
|
||||
import math
|
||||
from collections import Counter
|
||||
from enum import Enum
|
||||
from typing import Any, TypeVar
|
||||
|
||||
E = TypeVar("E", bound=Enum)
|
||||
|
||||
|
||||
def closed(enum: type[E], value: Any, fallback: E) -> E:
|
||||
"""The member for a value, or the fallback for one the enum doesn't know."""
|
||||
try:
|
||||
return enum(str(value))
|
||||
except ValueError:
|
||||
return fallback
|
||||
|
||||
|
||||
def rate(value: Any) -> float:
|
||||
"""A finite, non-negative number rounded to 2 decimals, else 0.
|
||||
|
||||
A NaN would serialize as null and fail the schema's number type.
|
||||
"""
|
||||
try:
|
||||
number = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
if not math.isfinite(number):
|
||||
return 0.0
|
||||
|
||||
return round(max(number, 0.0), 2)
|
||||
|
||||
|
||||
def histogram(counter: "Counter[E]") -> dict[E, int]:
|
||||
"""Drop the empty buckets, since an absent key means zero."""
|
||||
return {key: total for key, total in counter.items() if total > 0}
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Hardware section: CPU, memory, GPUs, decode and detection hardware, storage."""
|
||||
|
||||
import os
|
||||
from collections import Counter
|
||||
from typing import Any
|
||||
|
||||
import psutil
|
||||
|
||||
from frigate.analytics.collectors.common import closed, histogram
|
||||
from frigate.analytics.context import ReportContext
|
||||
from frigate.analytics.schema import (
|
||||
DecodeFamily,
|
||||
GpuInfo,
|
||||
GpuVendor,
|
||||
HardwareKey,
|
||||
HardwareSection,
|
||||
StorageInfo,
|
||||
)
|
||||
from frigate.const import RECORD_DIR
|
||||
from frigate.detectors.hardware import hardware_prober
|
||||
from frigate.util.hwaccel import hwaccel_options
|
||||
|
||||
DEVICE_TREE_MODEL = "/proc/device-tree/model"
|
||||
CPUINFO = "/proc/cpuinfo"
|
||||
|
||||
|
||||
def cpu_model() -> str:
|
||||
"""The board model on ARM boards, else the CPU's model name."""
|
||||
try:
|
||||
with open(DEVICE_TREE_MODEL) as f:
|
||||
board = f.read().strip("\x00\n ")
|
||||
|
||||
if board:
|
||||
return board[:64]
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
try:
|
||||
with open(CPUINFO) as f:
|
||||
for line in f:
|
||||
key, _, value = line.partition(":")
|
||||
|
||||
if key.strip() == "model name" and value.strip():
|
||||
return value.strip()[:64]
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return "unknown"
|
||||
|
||||
|
||||
def gpus(stats: dict[str, Any]) -> list[GpuInfo]:
|
||||
found: list[GpuInfo] = []
|
||||
|
||||
for name, entry in stats.get("gpu_usages", {}).items():
|
||||
vendor = entry.get("vendor") if isinstance(entry, dict) else None
|
||||
found.append(
|
||||
GpuInfo(
|
||||
vendor=closed(GpuVendor, vendor, GpuVendor.other),
|
||||
name=str(name)[:64],
|
||||
)
|
||||
)
|
||||
|
||||
return found
|
||||
|
||||
|
||||
def decode_families() -> list[DecodeFamily]:
|
||||
_, available = hwaccel_options()
|
||||
families = [
|
||||
closed(DecodeFamily, family.key, DecodeFamily.other) for family in available
|
||||
]
|
||||
return list(dict.fromkeys(families))
|
||||
|
||||
|
||||
def detection_hardware() -> dict[HardwareKey, int]:
|
||||
units: Counter[HardwareKey] = Counter()
|
||||
|
||||
for found in hardware_prober.probe():
|
||||
units[closed(HardwareKey, found.key, HardwareKey.other)] += found.count
|
||||
|
||||
return histogram(units)
|
||||
|
||||
|
||||
def storage(stats: dict[str, Any]) -> StorageInfo:
|
||||
# stats report sizes in MB
|
||||
entry = stats.get("service", {}).get("storage", {}).get(RECORD_DIR) or {}
|
||||
total_mb = float(entry.get("total") or 0)
|
||||
used_mb = float(entry.get("used") or 0)
|
||||
|
||||
return StorageInfo(
|
||||
record_fs=str(entry.get("mount_type") or "unknown")[:16],
|
||||
record_total_gb=round(total_mb / 1024),
|
||||
record_used_pct=min(round(used_mb / total_mb * 100), 100)
|
||||
if total_mb > 0
|
||||
else 0,
|
||||
)
|
||||
|
||||
|
||||
def collect(ctx: ReportContext) -> HardwareSection:
|
||||
return HardwareSection(
|
||||
cpu_model=cpu_model(),
|
||||
cpu_cores=os.cpu_count() or 0,
|
||||
memory_gb=round(psutil.virtual_memory().total / 2**30),
|
||||
gpus=gpus(ctx.stats),
|
||||
decode_families=decode_families(),
|
||||
detection_hardware=detection_hardware(),
|
||||
storage=storage(ctx.stats),
|
||||
)
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Install section: version, image variant, install type, platform."""
|
||||
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
|
||||
from frigate.analytics.collectors.common import closed
|
||||
from frigate.analytics.context import ReportContext
|
||||
from frigate.analytics.schema import Arch, ImageVariant, InstallSection, InstallType
|
||||
from frigate.version import VERSION
|
||||
|
||||
KERNEL_PATTERN = re.compile(r"^(\d{1,3})\.(\d{1,3})")
|
||||
|
||||
|
||||
def image_variant(value: str | None) -> ImageVariant:
|
||||
"""The published image from the build-time FRIGATE_IMAGE_VARIANT, dev when unset."""
|
||||
if not value:
|
||||
return ImageVariant.dev
|
||||
|
||||
return closed(ImageVariant, value, ImageVariant.other)
|
||||
|
||||
|
||||
def install_type() -> InstallType:
|
||||
# the add-on is a container too, so it has to be checked first
|
||||
if os.path.isfile("/data/options.json"):
|
||||
return InstallType.ha_addon
|
||||
|
||||
if os.environ.get("KUBERNETES_SERVICE_HOST"):
|
||||
return InstallType.kubernetes
|
||||
|
||||
if os.path.exists("/run/.containerenv"):
|
||||
return InstallType.podman
|
||||
|
||||
if os.path.exists("/.dockerenv"):
|
||||
return InstallType.docker
|
||||
|
||||
return InstallType.unknown
|
||||
|
||||
|
||||
def arch(machine: str) -> Arch:
|
||||
match machine.lower():
|
||||
case "x86_64" | "amd64":
|
||||
return Arch.x86_64
|
||||
case "aarch64" | "arm64":
|
||||
return Arch.aarch64
|
||||
case _:
|
||||
return Arch.other
|
||||
|
||||
|
||||
def kernel(release: str) -> str:
|
||||
match = KERNEL_PATTERN.match(release)
|
||||
return f"{match.group(1)}.{match.group(2)}" if match else "unknown"
|
||||
|
||||
|
||||
def collect(ctx: ReportContext) -> InstallSection:
|
||||
return InstallSection(
|
||||
version=VERSION[:32],
|
||||
image_variant=image_variant(os.environ.get("FRIGATE_IMAGE_VARIANT")),
|
||||
install_type=install_type(),
|
||||
arch=arch(platform.machine()),
|
||||
kernel=kernel(platform.release()),
|
||||
run_as_root=os.geteuid() == 0,
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
"""What the collectors read, gathered once per report."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from frigate.config import FrigateConfig
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReportContext:
|
||||
config: FrigateConfig
|
||||
stats: dict[str, Any]
|
||||
notice_stats: list[dict[str, Any]] = field(default_factory=list)
|
||||
Reference in New Issue
Block a user