mirror of
https://github.com/infiniteCable2/openpilot.git
synced 2026-08-06 16:55:54 +08:00
stats: sunnylink integration (#1454)
* sunnylink: add statsd process and related telemetry logging infrastructure - Introduced `statsd_sp` process for handling Sunnylink-specific stats. - Enhanced metrics logging with improved directory structure and data handling. * sunnylink: re-enable and refine stat_handler for telemetry processing - Reactivated `stat_handler` thread with improved path handling. - Made `stat_handler` more flexible by allowing directory injection. * statsd: fix formatting issue in telemetry string generation - Corrected missing comma between `sunnylink_dongle_id` and `comma_dongle_id`. * update statsd_sp process configuration for enhanced readiness logic - Modified `statsd_sp` initialization to include `always_run` alongside `sunnylink_ready_shim`. - Ensures robust process activation conditions. * refactor(statsd): enhance and unify StatLogSP implementation - Replaced custom `StatLogSP` in sunnylink with centralized implementation from `system.statsd`. - Ensures consistent logic for StatLogSP handling across modules. * fix * refactor(statsd): add intercept parameter to StatLogSP for configurable logging - Introduced optional `intercept` parameter to `StatLogSP` to manage `comma_statlog` initialization. - Updated usage in `sunnylink` to disable interception where unnecessary. * Dont complain * feat(statsd): add raw metric type and SunnyPilot-specific stats collection - Introduced `METRIC_TYPE.RAW` for base64-encoded raw data metrics. - Added `sp_stats` thread to export SunnyPilot params as raw metrics. - Enhanced telemetry handling with decoding and serialization updates. * refactor(statsd): improve `sp_stats` error handling and param processing - Enhanced exception handling for `params.get` to prevent crashes. - Added support for nested dict values to be included in stats. * refactor(statsd): adjust imports and minor code formatting updates - Updated `Ratekeeper` import path for consistency with the `openpilot` module structure. - Fixed minor formatting for improved readability. * refactor(statsd): update typings and remove unused NoReturn annotation - Removed unnecessary `NoReturn` typing for `stats_main` to simplify function definition. - Adjusted `get_influxdb_line_raw` to refine typing for `value` parameter. * cleanup * init * init * slightly more * staticmethod * handle them all * get them models * log with route * more * car * Revert "car" This reverts commit fe1c90cf4d422864388e4a73d5cd25c9e585a9b3. * handle capnp * Revert "handle capnp" This reverts commit c5aea6880333863838a7c258541fb7a2812f2e2b. * 1 more time * Revert "1 more time" This reverts commit a364474fa580d9ef9760a8896f9db76d526abead. * Cleaning to expose wider * feat(interfaces, statsd): log car params to stats system - Added `STATSLOGSP` import and logging to capture `carFingerprint` in metrics. - Improved error handling in `get_influxdb_line_raw` for robust metric generation. * refactor(interfaces): streamline car params logging to stats - Simplified logging by directly converting `CP` to a dictionary. - Removed legacy stats aggregation for clarity. * feat(sunnylink): enable compression for stats in SunnyLink - Added optional compression for stats payload to support large data. - Updated `stat_handler` to handle compression and base64 encoding. * fix(statsd): filter complex types in `get_influxdb_line_raw` - Skips unsupported types (dict, list, bytes) to prevent formatting errors. - Simplifies type annotation for `value` parameter. * fix(statsd): use `json.dumps` for string conversion in `get_influxdb_line_raw` - Ensures proper handling of special characters in values. - Prevents potential formatting issues with raw `str()` conversion. * refactor(interfaces, statsd): update parameter keys for stats logging - Renamed logged keys for better clarity (`sunnypilot_params` → `sunnypilot.car_params`, `device_params`). - Ensures consistency across data logs. * bet --------- Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
This commit is contained in:
@@ -744,26 +744,40 @@ def log_handler(end_event: threading.Event, log_attr_name=LOG_ATTR_NAME) -> None
|
||||
cloudlog.exception("athena.log_handler.exception")
|
||||
|
||||
|
||||
def stat_handler(end_event: threading.Event) -> None:
|
||||
STATS_DIR = Paths.stats_root()
|
||||
def stat_handler(end_event: threading.Event, stats_dir=None, is_sunnylink=False) -> None:
|
||||
stats_dir = stats_dir or Paths.stats_root()
|
||||
last_scan = 0.0
|
||||
|
||||
while not end_event.is_set():
|
||||
curr_scan = time.monotonic()
|
||||
try:
|
||||
if curr_scan - last_scan > 10:
|
||||
stat_filenames = list(filter(lambda name: not name.startswith(tempfile.gettempprefix()), os.listdir(STATS_DIR)))
|
||||
stat_filenames = list(filter(lambda name: not name.startswith(tempfile.gettempprefix()), os.listdir(stats_dir)))
|
||||
if len(stat_filenames) > 0:
|
||||
stat_path = os.path.join(STATS_DIR, stat_filenames[0])
|
||||
stat_path = os.path.join(stats_dir, stat_filenames[0])
|
||||
with open(stat_path) as f:
|
||||
payload = f.read()
|
||||
is_compressed = False
|
||||
|
||||
# Log the current size of the file
|
||||
if is_sunnylink:
|
||||
# Compress and encode the data if it exceeds the maximum size
|
||||
compressed_data = gzip.compress(payload.encode())
|
||||
payload = base64.b64encode(compressed_data).decode()
|
||||
is_compressed = True
|
||||
|
||||
jsonrpc = {
|
||||
"method": "storeStats",
|
||||
"params": {
|
||||
"stats": f.read()
|
||||
"stats": payload
|
||||
},
|
||||
"jsonrpc": "2.0",
|
||||
"id": stat_filenames[0]
|
||||
}
|
||||
|
||||
if is_sunnylink and is_compressed:
|
||||
jsonrpc["params"]["compressed"] = is_compressed
|
||||
|
||||
low_priority_send_queue.put_nowait(json.dumps(jsonrpc))
|
||||
os.remove(stat_path)
|
||||
last_scan = curr_scan
|
||||
|
||||
@@ -55,6 +55,13 @@ class Paths:
|
||||
else:
|
||||
return "/data/stats/"
|
||||
|
||||
@staticmethod
|
||||
def stats_sp_root() -> str:
|
||||
if PC:
|
||||
return str(Path(Paths.comma_home()) / "stats")
|
||||
else:
|
||||
return "/data/stats_sp/"
|
||||
|
||||
@staticmethod
|
||||
def config_root() -> str:
|
||||
if PC:
|
||||
|
||||
@@ -164,6 +164,7 @@ procs = [
|
||||
# sunnylink <3
|
||||
DaemonProcess("manage_sunnylinkd", "sunnypilot.sunnylink.athena.manage_sunnylinkd", "SunnylinkdPid"),
|
||||
PythonProcess("sunnylink_registration_manager", "sunnypilot.sunnylink.registration_manager", sunnylink_need_register_shim),
|
||||
PythonProcess("statsd_sp", "sunnypilot.sunnylink.statsd", and_(always_run, sunnylink_ready_shim)),
|
||||
]
|
||||
|
||||
# sunnypilot
|
||||
|
||||
+55
-4
@@ -1,11 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from decimal import Decimal
|
||||
|
||||
import zmq
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, UTC
|
||||
from datetime import datetime, UTC, date
|
||||
from typing import NoReturn
|
||||
|
||||
from openpilot.common.params import Params
|
||||
@@ -21,18 +25,21 @@ from openpilot.system.loggerd.config import STATS_DIR_FILE_LIMIT, STATS_SOCKET,
|
||||
class METRIC_TYPE:
|
||||
GAUGE = 'g'
|
||||
SAMPLE = 'sa'
|
||||
RAW = 'r'
|
||||
|
||||
|
||||
class StatLog:
|
||||
def __init__(self):
|
||||
self.pid = None
|
||||
self.zctx = None
|
||||
self.sock = None
|
||||
self.stats_socket = STATS_SOCKET
|
||||
|
||||
def connect(self) -> None:
|
||||
self.zctx = zmq.Context()
|
||||
self.zctx = zmq.Context.instance() or zmq.Context()
|
||||
self.sock = self.zctx.socket(zmq.PUSH)
|
||||
self.sock.setsockopt(zmq.LINGER, 10)
|
||||
self.sock.connect(STATS_SOCKET)
|
||||
self.sock.connect(self.stats_socket)
|
||||
self.pid = os.getpid()
|
||||
|
||||
def __del__(self):
|
||||
@@ -60,6 +67,50 @@ class StatLog:
|
||||
self._send(f"{name}:{value}|{METRIC_TYPE.SAMPLE}")
|
||||
|
||||
|
||||
class StatLogSP(StatLog):
|
||||
def __init__(self, intercept=True):
|
||||
"""
|
||||
Initializes the class instance with an optional parameter to determine
|
||||
if statistical logging should be configured or not.
|
||||
|
||||
:param intercept: A boolean flag that indicates whether to initialize
|
||||
the `comma_statlog`. If True, the `comma_statlog` attribute is
|
||||
instantiated as a `StatLog` object. Defaults to True.
|
||||
"""
|
||||
super().__init__()
|
||||
self.comma_statlog = StatLog() if intercept else None
|
||||
self.stats_socket = f"{STATS_SOCKET}_sp"
|
||||
|
||||
def connect(self) -> None:
|
||||
super().connect()
|
||||
if self.comma_statlog:
|
||||
self.comma_statlog.connect()
|
||||
|
||||
def __del__(self):
|
||||
super().__del__()
|
||||
if self.comma_statlog:
|
||||
self.comma_statlog.__del__()
|
||||
|
||||
def _send(self, metric: str) -> None:
|
||||
super()._send(metric)
|
||||
if self.comma_statlog:
|
||||
self.comma_statlog._send(metric)
|
||||
|
||||
@staticmethod
|
||||
def default_converter(obj):
|
||||
if isinstance(obj, (datetime, date)):
|
||||
return obj.isoformat()
|
||||
if isinstance(obj, set):
|
||||
return list(obj)
|
||||
if isinstance(obj, Decimal):
|
||||
return float(obj)
|
||||
return str(obj) # fallback for unknown types
|
||||
|
||||
def raw(self, name: str, value: dict) -> None:
|
||||
encoded_dict = base64.b64encode(json.dumps(value, default=self.default_converter).encode("utf-8")).decode("utf-8")
|
||||
self._send(f"{name}:{encoded_dict}|{METRIC_TYPE.RAW}")
|
||||
|
||||
|
||||
def main() -> NoReturn:
|
||||
dongle_id = Params().get("DongleId")
|
||||
def get_influxdb_line(measurement: str, value: float | dict[str, float], timestamp: datetime, tags: dict) -> str:
|
||||
@@ -180,4 +231,4 @@ def main() -> NoReturn:
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
else:
|
||||
statlog = StatLog()
|
||||
statlog = StatLogSP(intercept=True)
|
||||
|
||||
Reference in New Issue
Block a user