159 lines
5.6 KiB
Python
159 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
import importlib.util
|
|
import os
|
|
import sys
|
|
import urllib.request
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
|
|
MODULE_PATH = Path(__file__).with_name("aw-worktime-api.py")
|
|
SPEC = importlib.util.spec_from_file_location("aw_worktime_api", MODULE_PATH)
|
|
WORKTIME = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(WORKTIME)
|
|
|
|
INFLUX_URL = os.environ.get("AW_WORKTIME_INFLUX_URL", "").strip().rstrip("/")
|
|
INFLUX_ORG = os.environ.get("AW_WORKTIME_INFLUX_ORG", "proxmox").strip() or "proxmox"
|
|
INFLUX_BUCKET = os.environ.get("AW_WORKTIME_INFLUX_BUCKET", "aw_metrics").strip() or "aw_metrics"
|
|
INFLUX_TOKEN = os.environ.get("AW_WORKTIME_INFLUX_TOKEN", "").strip()
|
|
INFLUX_ENABLED = os.environ.get("AW_WORKTIME_INFLUX_ENABLED", "").strip().lower() in {"1", "true", "yes", "on"}
|
|
HOSTS = [item.strip() for item in os.environ.get("AW_WORKTIME_INFLUX_HOSTS", WORKTIME.DEFAULT_HOST).split(",") if item.strip()]
|
|
DAYS = [item.strip() for item in os.environ.get("AW_WORKTIME_INFLUX_DAYS", "today,yesterday").split(",") if item.strip()]
|
|
|
|
|
|
def _escape_tag(value):
|
|
return (
|
|
str(value or "")
|
|
.replace("\\", "\\\\")
|
|
.replace(" ", "\\ ")
|
|
.replace(",", "\\,")
|
|
.replace("=", "\\=")
|
|
)
|
|
|
|
|
|
def _line(measurement, tags, fields, timestamp_ns):
|
|
tag_part = ",".join(f"{key}={_escape_tag(value)}" for key, value in sorted(tags.items()) if value is not None and value != "")
|
|
field_parts = []
|
|
for key, value in fields.items():
|
|
if isinstance(value, bool):
|
|
field_parts.append(f"{key}={'true' if value else 'false'}")
|
|
elif isinstance(value, int):
|
|
field_parts.append(f"{key}={value}i")
|
|
elif isinstance(value, float):
|
|
field_parts.append(f"{key}={value}")
|
|
else:
|
|
text = str(value or "").replace("\\", "\\\\").replace('"', '\\"')
|
|
field_parts.append(f'{key}="{text}"')
|
|
if not field_parts:
|
|
return ""
|
|
if tag_part:
|
|
return f"{measurement},{tag_part} {','.join(field_parts)} {timestamp_ns}"
|
|
return f"{measurement} {','.join(field_parts)} {timestamp_ns}"
|
|
|
|
|
|
def _timestamp_ns(dt):
|
|
return int(dt.astimezone(timezone.utc).timestamp() * 1_000_000_000)
|
|
|
|
|
|
def build_lines_for_day(host, report_date):
|
|
bounds, events = WORKTIME.fetch_events_for_date(host, report_date)
|
|
rows = WORKTIME.aggregate_rows(events, bounds["start"], bounds["end"], host)
|
|
hourly_rows = WORKTIME.aggregate_hourly_rows(events, bounds["start"], bounds["end"], host)
|
|
summary = WORKTIME.build_report_summary(rows)
|
|
|
|
lines = []
|
|
daily_ts = _timestamp_ns(bounds["start"])
|
|
|
|
for row in rows:
|
|
lines.append(
|
|
_line(
|
|
"aw_rdp_worktime_daily",
|
|
{
|
|
"host": host,
|
|
"user": row["user"],
|
|
"user_id": row["user_id"],
|
|
"report_date": report_date.isoformat(),
|
|
},
|
|
{
|
|
"active_seconds": int(row["active_seconds"]),
|
|
"idle_seconds": int(row["idle_seconds"]),
|
|
"sessions_count": int(row["sessions_count"]),
|
|
"samples_count": int(row["samples_count"]),
|
|
"active_samples": int(row["active_samples"]),
|
|
},
|
|
daily_ts,
|
|
)
|
|
)
|
|
|
|
for row in hourly_rows:
|
|
lines.append(
|
|
_line(
|
|
"aw_rdp_worktime_hourly",
|
|
{
|
|
"host": host,
|
|
"user": row["user"],
|
|
"user_id": row["user_id"],
|
|
"report_date": row["report_date"],
|
|
"hour_local": row["hour_local"],
|
|
},
|
|
{
|
|
"active_seconds": int(row["active_seconds"]),
|
|
},
|
|
_timestamp_ns(WORKTIME.pts(row["bucket_start_utc"])),
|
|
)
|
|
)
|
|
|
|
lines.append(
|
|
_line(
|
|
"aw_rdp_worktime_summary_daily",
|
|
{
|
|
"host": host,
|
|
"report_date": report_date.isoformat(),
|
|
},
|
|
{
|
|
"users_count": int(summary["users_count"]),
|
|
"total_active_seconds": int(summary["total_active_seconds"]),
|
|
"top_user": summary["top_user"],
|
|
},
|
|
daily_ts,
|
|
)
|
|
)
|
|
return [line for line in lines if line]
|
|
|
|
|
|
def write_lines(lines):
|
|
if not lines:
|
|
return 0
|
|
if not INFLUX_URL or not INFLUX_TOKEN:
|
|
raise RuntimeError("InfluxDB destination is not configured")
|
|
payload = ("\n".join(lines) + "\n").encode("utf-8")
|
|
req = urllib.request.Request(
|
|
f"{INFLUX_URL}/api/v2/write?org={INFLUX_ORG}&bucket={INFLUX_BUCKET}&precision=ns",
|
|
data=payload,
|
|
method="POST",
|
|
headers={"Authorization": f"Token {INFLUX_TOKEN}", "Content-Type": "text/plain; charset=utf-8"},
|
|
)
|
|
with urllib.request.urlopen(req, timeout=30) as response:
|
|
if response.status not in {204, 200}:
|
|
raise RuntimeError(f"InfluxDB write failed with status={response.status}")
|
|
return len(lines)
|
|
|
|
|
|
def main():
|
|
if not INFLUX_ENABLED:
|
|
print("[aw-worktime-influx-exporter] disabled by AW_WORKTIME_INFLUX_ENABLED", file=sys.stderr)
|
|
return 0
|
|
|
|
lines = []
|
|
for host in HOSTS:
|
|
for day in DAYS:
|
|
report_date = WORKTIME.resolve_report_date(day=day)
|
|
lines.extend(build_lines_for_day(host, report_date))
|
|
written = write_lines(lines)
|
|
print(f"[aw-worktime-influx-exporter] wrote {written} points to {INFLUX_BUCKET}", file=sys.stderr)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|