#!/bin/bash
# POS-179：只读统计门店 Apache 访问日志里 Complex.php?op=orderFastWithTseInfo 的调用方，由设备命令 ORDER_FAST_TSE_PROBE
# 以 aaden 跑（aaden 在 adm 组，读得了 /var/log/apache2；不 sudo、不接受参数）。只读日志，不改任何东西，也不落临时文件：
# 一遍读完在 awk 内存里聚合，只输出聚合结果（按天、方法和状态、User-Agent、来源类别、局域网客户端数、参数名），
# 不输出原始日志行、客户端 IP 和任何参数值。
#
# 只能看到 URL 里的东西：op 在 query 里，cashBoxId 在 POST body 里，访问日志看不到（要靠门店库里的订单另查）。
# Ubuntu 的 apache2 logrotate 默认按天轮转、留 14 份，所以最多回看约 14 天。
# 来源 local = 127.0.0.1：本机调用和从 ngrok 进来的外网调用都算在这里，要结合 User-Agent 看。
# 读不到任何日志文件时退出码 2（不是「没人调用」）。
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export LC_ALL=C
umask 077
# 只给自测留的口子，设备上从不设置
DIR="${AADEN_PROBE_LOG_DIR:-/var/log/apache2}"

section() { echo "== $1"; }
# 营业时间也可能被下发：整条管道（zcat/tr/grep/awk）都让着收银。ionice 只在 BFQ/CFQ 下生效，没有就算了
renice -n 19 $$ >/dev/null 2>&1
command -v ionice >/dev/null 2>&1 && ionice -c3 -p $$ >/dev/null 2>&1

section version
echo "probe=3"

section access
echo "user=$(id -un) groups=$(id -Gn | tr ' ' ',')"
if [ -d "$DIR" ] && [ -r "$DIR" ] && [ -x "$DIR" ]; then echo "dir=readable"; else echo "dir=unreadable"; fi
readable=0; unreadable=0
for f in "$DIR"/access.log* "$DIR"/other_vhosts_access.log*; do
  [ -e "$f" ] || continue
  if [ -r "$f" ]; then readable=$((readable + 1)); else unreadable=$((unreadable + 1)); fi
done
corrupt=0
for f in "$DIR"/*.gz; do
  [ -r "$f" ] || continue
  gzip -t "$f" >/dev/null 2>&1 || corrupt=$((corrupt + 1))
done
echo "files_readable=$readable files_unreadable=$unreadable files_corrupt_gz=$corrupt"

section log_files
for f in "$DIR"/access.log* "$DIR"/other_vhosts_access.log*; do
  [ -e "$f" ] || continue
  echo "$(date -r "$f" +%F 2>/dev/null) $(stat -c %s "$f" 2>/dev/null) $(basename "$f") $([ -r "$f" ] && echo r || echo unreadable)"
done | head -40

if [ "$readable" = 0 ]; then
  section end
  echo "没有读得到的访问日志，结果不能当成「没人调用」"
  exit 2
fi

# 每个文件：zcat -f（.gz 和普通文件都行）→ 去掉断电留下的 NUL → grep -a（不让 grep 当二进制丢行）→ 统一成一种格式交给聚合
# other_vhosts 是 vhost_combined：第一列多一个 %v:%p，去掉后和 combined 一样
emit() {
  local f
  for f in "$DIR"/access.log* "$DIR"/other_vhosts_access.log*; do
    [ -r "$f" ] || continue
    case "$(basename "$f")" in
      other_vhosts*) zcat -f "$f" 2>/dev/null | tr -d '\000' | grep -a -F 'op=orderFastWithTseInfo' | sed -E 's/^[^ ]+ //' ;;
      *) zcat -f "$f" 2>/dev/null | tr -d '\000' | grep -a -F 'op=orderFastWithTseInfo' ;;
    esac
  done
}

emit | awk -F'"' '
BEGIN {
  # 门店的 awk 多是 mawk：不用 {m,n} 区间写法
  split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec", m, " ")
  for (i = 1; i <= 12; i++) mon[m[i]] = sprintf("%02d", i)
}
{
  # Apache 把请求行 / Referer / UA 里的 " 记成 \"：先换成占位符再分段，否则段落错位会把参数值当成方法、状态或 UA 输出
  if (index($0, "\\") > 0) {
    gsub(/\\\\/, "\001", $0)
    gsub(/\\"/, "\002", $0)
  }
  # 只认请求行（第一对引号）里的 op，不算 Referer 里的
  req = $2
  if (req !~ /[?&]op=orderFastWithTseInfo([& ]|$)/) next
  total++
  split($1, pre, " ")
  ip = pre[1]
  if (match($1, /\[[0-9]+\/[A-Za-z]+\/[0-9]+/)) {
    d = substr($1, RSTART + 1, RLENGTH - 1); split(d, dp, "/")
    day[dp[3] "-" mon[dp[2]] "-" dp[1]]++
  }
  split(req, rq, " "); split($3, st, " ")
  # 白名单兜底：方法只认大写字母、状态只认三位数字，别的一律 <bad>
  meth = (rq[1] ~ /^[A-Z]+$/ && length(rq[1]) <= 10) ? rq[1] : "<bad>"
  stat = (st[1] ~ /^[0-9][0-9][0-9]$/) ? st[1] : "<bad>"
  ms[meth " " stat]++
  # combined 才有 Referer 和 UA（至少 7 段）；common 格式没有 UA，绝不能退回去取别的段（会把带参数的请求行带出去）
  if (NF >= 7) { ua = $6; if (length(ua) > 160) ua = substr(ua, 1, 160) "…" } else ua = "<no-ua>"
  if (ua == "") ua = "<empty-ua>"
  gsub(/[\001\002]/, "?", ua)
  uas[ua]++
  sub(/^::ffff:/, "", ip)
  if (ip ~ /^(127\.|::1$)/) src["local"]++
  else if (ip ~ /^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|fe80:)/) { src["lan"]++; lanip[ip] = 1 }
  else src["other"]++
  q = rq[2]; sub(/^[^?]*\?/, "", q); n = split(q, kv, "&")
  for (i = 1; i <= n; i++) { k = kv[i]; if (index(k, "=") == 0) { keys["<noeq>"]++; continue }; sub(/=.*/, "", k); if (k ~ /^[A-Za-z0-9_]+$/ && length(k) <= 40) keys[k]++; else keys["<other>"]++ }
}
END {
  print "== total"; print total + 0
  print "== by_day"; for (k in day) print k, day[k] | "sort"; close("sort")
  print "== by_method_status"; for (k in ms) print ms[k], k | "sort -rn | head -10"; close("sort -rn | head -10")
  print "== by_user_agent"; for (k in uas) print uas[k], k | "sort -rn | head -20"; close("sort -rn | head -20")
  print "== by_source"; for (k in src) print src[k], k
  c = 0; for (k in lanip) c++
  print "== distinct_lan_clients"; print c
  print "== query_keys"; for (k in keys) print keys[k], k | "sort -rn | head -15"; close("sort -rn | head -15")
}'
rc=${PIPESTATUS[1]}
if [ "$rc" != 0 ]; then
  section end
  echo "聚合失败（awk 退出码 $rc），上面的统计不完整，不能当成「没人调用」"
  exit 1
fi

section end
exit 0
