fix(worktime): restore real per-user RDP report pipeline
This commit is contained in:
+119
-27
@@ -5,6 +5,9 @@ DAY=""
|
||||
FROM=""
|
||||
TO=""
|
||||
AW_BASE_URL="${AW_BASE_URL:-http://10.10.10.13:5600/api/0}"
|
||||
AW_WORKTIME_HOST="${AW_WORKTIME_HOST:-SHARKON2025}"
|
||||
AW_WORKTIME_DEFAULT_SAMPLE_SECONDS="${AW_WORKTIME_DEFAULT_SAMPLE_SECONDS:-30}"
|
||||
AW_WORKTIME_MAX_SAMPLE_SECONDS="${AW_WORKTIME_MAX_SAMPLE_SECONDS:-300}"
|
||||
OUT_DIR="${OUT_DIR:-reports}"
|
||||
|
||||
usage() {
|
||||
@@ -14,6 +17,7 @@ Usage:
|
||||
$0 --from YYYY-MM-DD --to YYYY-MM-DD
|
||||
Env:
|
||||
AW_BASE_URL (default: ${AW_BASE_URL})
|
||||
AW_WORKTIME_HOST (default: ${AW_WORKTIME_HOST})
|
||||
OUT_DIR (default: ${OUT_DIR})
|
||||
EOF
|
||||
}
|
||||
@@ -50,14 +54,19 @@ mkdir -p "$OUT_DIR"
|
||||
CSV_OUT="${OUT_DIR}/rdp-worktime-${FROM}_${TO}.csv"
|
||||
JSON_OUT="${OUT_DIR}/rdp-worktime-${FROM}_${TO}.json"
|
||||
|
||||
python3 - "$AW_BASE_URL" "$FROM" "$TO" "$CSV_OUT" "$JSON_OUT" <<'PY'
|
||||
python3 - "$AW_BASE_URL" "$AW_WORKTIME_HOST" "$AW_WORKTIME_DEFAULT_SAMPLE_SECONDS" "$AW_WORKTIME_MAX_SAMPLE_SECONDS" "$FROM" "$TO" "$CSV_OUT" "$JSON_OUT" <<'PY'
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
base, from_d, to_d, csv_out, json_out = sys.argv[1:6]
|
||||
base, host, default_sample, max_sample, from_d, to_d, csv_out, json_out = sys.argv[1:9]
|
||||
base = (base or "http://10.10.10.13:5600").rstrip("/")
|
||||
if not base.endswith("/api/0"):
|
||||
base = base + "/api/0"
|
||||
default_sample = max(1.0, float(default_sample))
|
||||
max_sample = max(default_sample, float(max_sample))
|
||||
|
||||
def get_json(url: str):
|
||||
with urllib.request.urlopen(url, timeout=30) as r:
|
||||
@@ -68,21 +77,55 @@ def parse_ts(s):
|
||||
return None
|
||||
return datetime.fromisoformat(s.replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||
|
||||
buckets = get_json(f"{base}/buckets")
|
||||
sessions_bucket = None
|
||||
for k in buckets.keys():
|
||||
if k.startswith("aw-worktime-sessions_"):
|
||||
sessions_bucket = k
|
||||
break
|
||||
def clamp_seconds(value, fallback=default_sample):
|
||||
try:
|
||||
seconds = float(value)
|
||||
except Exception:
|
||||
seconds = float(fallback)
|
||||
if seconds <= 0:
|
||||
seconds = float(fallback)
|
||||
return min(seconds, max_sample)
|
||||
|
||||
if not sessions_bucket:
|
||||
raise SystemExit("No aw-worktime-sessions_* bucket found")
|
||||
def merge_intervals(intervals):
|
||||
if not intervals:
|
||||
return []
|
||||
intervals = sorted(intervals, key=lambda item: item[0])
|
||||
merged = [intervals[0]]
|
||||
for start, end in intervals[1:]:
|
||||
last_start, last_end = merged[-1]
|
||||
if start <= last_end:
|
||||
if end > last_end:
|
||||
merged[-1] = (last_start, end)
|
||||
continue
|
||||
merged.append((start, end))
|
||||
return merged
|
||||
|
||||
def is_active(data):
|
||||
state = str(data.get("state") or "").strip().lower()
|
||||
if isinstance(data.get("active"), bool) and data.get("active"):
|
||||
return True
|
||||
return ("актив" in state) or (state == "active")
|
||||
|
||||
def normalize_user_id(data, host_name, username):
|
||||
raw = str(data.get("userId") or "").strip()
|
||||
if raw and "\\" in raw:
|
||||
_, right = raw.split("\\", 1)
|
||||
return f"{host_name}\\{right}"
|
||||
if raw:
|
||||
return raw
|
||||
return f"{host_name}\\{username}"
|
||||
|
||||
bucket_id = f"aw-worktime-sessions_{host}"
|
||||
try:
|
||||
get_json(f"{base}/buckets/{bucket_id}")
|
||||
except Exception:
|
||||
raise SystemExit(f"Bucket not found: {bucket_id}")
|
||||
|
||||
start = datetime.fromisoformat(from_d + "T00:00:00+00:00")
|
||||
end = datetime.fromisoformat(to_d + "T23:59:59+00:00")
|
||||
|
||||
ev = get_json(f"{base}/buckets/{sessions_bucket}/events?limit=20000")
|
||||
by_user = {}
|
||||
ev = get_json(f"{base}/buckets/{bucket_id}/events?limit=50000")
|
||||
by_identity = {}
|
||||
for e in ev:
|
||||
ts = parse_ts(e.get("timestamp"))
|
||||
if ts is None or ts < start or ts > end:
|
||||
@@ -91,40 +134,89 @@ for e in ev:
|
||||
user = (d.get("username") or "").strip()
|
||||
if not user:
|
||||
continue
|
||||
state = (d.get("state") or "").strip().lower()
|
||||
is_active = ("актив" in state) or (state == "active")
|
||||
rec = by_user.setdefault(user, {"active_ts": set(), "first": None, "last": None, "rows": 0})
|
||||
rec["rows"] += 1
|
||||
if is_active:
|
||||
rec["active_ts"].add(ts.replace(microsecond=0))
|
||||
rec["first"] = ts if rec["first"] is None or ts < rec["first"] else rec["first"]
|
||||
rec["last"] = ts if rec["last"] is None or ts > rec["last"] else rec["last"]
|
||||
session_id = str(d.get("sessionId") or "").strip() or "unknown"
|
||||
by_identity.setdefault((user, session_id), []).append({
|
||||
"ts": ts,
|
||||
"duration": e.get("duration"),
|
||||
"data": d,
|
||||
})
|
||||
|
||||
rows = []
|
||||
full_range = int((end - start).total_seconds())
|
||||
full_range = int((end - start).total_seconds()) + 1
|
||||
by_user = {}
|
||||
for (user, session_id), samples in by_identity.items():
|
||||
samples = sorted(samples, key=lambda item: item["ts"])
|
||||
for idx, sample in enumerate(samples):
|
||||
data = sample["data"]
|
||||
rec = by_user.setdefault(user, {
|
||||
"user": user,
|
||||
"user_id": normalize_user_id(data, host, user),
|
||||
"sessions": set(),
|
||||
"samples_count": 0,
|
||||
"active_samples": 0,
|
||||
"intervals": [],
|
||||
})
|
||||
rec["sessions"].add(session_id)
|
||||
rec["samples_count"] += 1
|
||||
if not is_active(data):
|
||||
continue
|
||||
rec["active_samples"] += 1
|
||||
sample_seconds = None
|
||||
for key in ("sampleSeconds", "pollSeconds"):
|
||||
value = data.get(key)
|
||||
try:
|
||||
if float(value) > 0:
|
||||
sample_seconds = clamp_seconds(value)
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if sample_seconds is None:
|
||||
try:
|
||||
duration = float(sample.get("duration") or 0.0)
|
||||
except Exception:
|
||||
duration = 0.0
|
||||
if duration > 0:
|
||||
sample_seconds = clamp_seconds(duration)
|
||||
else:
|
||||
next_ts = samples[idx + 1]["ts"] if idx + 1 < len(samples) else None
|
||||
if next_ts is not None:
|
||||
sample_seconds = clamp_seconds((next_ts - sample["ts"]).total_seconds())
|
||||
else:
|
||||
sample_seconds = clamp_seconds(default_sample)
|
||||
interval_end = min(sample["ts"] + timedelta(seconds=sample_seconds), end + timedelta(seconds=1))
|
||||
if interval_end > sample["ts"]:
|
||||
rec["intervals"].append((sample["ts"], interval_end))
|
||||
|
||||
for user in sorted(by_user.keys()):
|
||||
rec = by_user[user]
|
||||
active = len(rec["active_ts"])
|
||||
merged = merge_intervals(rec["intervals"])
|
||||
active = int(sum((finish - begin).total_seconds() for begin, finish in merged))
|
||||
active = min(active, full_range)
|
||||
idle = max(0, full_range - active)
|
||||
rows.append({
|
||||
"user": user,
|
||||
"user": rec["user"],
|
||||
"user_id": rec["user_id"],
|
||||
"active_seconds": int(active),
|
||||
"active_hhmm": f"{int(active)//3600:02d}:{(int(active)%3600)//60:02d}",
|
||||
"first_activity": rec["first"].isoformat().replace("+00:00","Z") if rec["first"] else "",
|
||||
"last_activity": rec["last"].isoformat().replace("+00:00","Z") if rec["last"] else "",
|
||||
"first_activity": merged[0][0].isoformat().replace("+00:00","Z") if merged else "",
|
||||
"last_activity": merged[-1][1].isoformat().replace("+00:00","Z") if merged else "",
|
||||
"idle_seconds": int(idle),
|
||||
"sessions_count": rec["rows"],
|
||||
"sessions_count": len(rec["sessions"]),
|
||||
"samples_count": rec["samples_count"],
|
||||
"active_samples": rec["active_samples"],
|
||||
})
|
||||
|
||||
with open(csv_out, "w", newline="", encoding="utf-8") as f:
|
||||
w = csv.DictWriter(f, fieldnames=[
|
||||
"user","active_seconds","active_hhmm","first_activity","last_activity","idle_seconds","sessions_count"
|
||||
"user","user_id","active_seconds","active_hhmm","first_activity","last_activity","idle_seconds","sessions_count","samples_count","active_samples"
|
||||
])
|
||||
w.writeheader()
|
||||
w.writerows(rows)
|
||||
|
||||
with open(json_out, "w", encoding="utf-8") as f:
|
||||
json.dump({
|
||||
"host": host,
|
||||
"bucket_id": bucket_id,
|
||||
"from": from_d,
|
||||
"to": to_d,
|
||||
"generated_at_utc": datetime.now(timezone.utc).isoformat().replace("+00:00","Z"),
|
||||
|
||||
Reference in New Issue
Block a user