diff --git a/deploy/README.md b/deploy/README.md index eac95e7..75788d6 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -60,6 +60,71 @@ PR 머지 → main push → [CI: build-and-test(필수)] → [Deploy 워크플 5. **첫 태그 파일**: `printf 'INNOLIVE_TAG=<현재 배포 커밋SHA>\n' > /opt/innolive/deploy/current_tag` 6. 구 preflight 우회(`/etc/innolive/preflight-off.env`)는 유닛 교체와 함께 제거한다. +## 런타임 로그 보관 (#268) + +journald는 용량 상한에 닿으면 오래된 로그부터 지운다. `log-archive.sh`가 `innolive-server`의 +전체 런타임 로그를 **매일 00:10**(`innolive-log-archive.timer`)과 **배포 재시작 직전** +(`apply-release.sh`)에 보관 디스크로 옮긴다. + +- 파일: `/srv/innolive-logs/innolive-server-YYYY-MM-DD.log.zst` (서버 현지 날짜, 하루 한 파일, root:adm 640) +- 없는 날은 채우고 어제·오늘은 다시 쓴다. 첫 실행은 journald에 남은 가장 오래된 날부터 채운다. +- 스트림 키·토큰·비밀번호는 가린다. 사용자 IP(ICE 후보)는 장애 조사용으로 남는다. +- `/srv/innolive-logs`가 마운트 지점이 아니면 쓰지 않는다(루트 디스크에 잘못 쌓이는 것 방지). +- 배포 중 보관이 실패해도 배포는 진행한다. 사유는 `/opt/innolive/deploy/logs/`의 상세 로그에만 남는다. + +### 보관 디스크 준비 (1회, root) +미사용 디스크(sda)의 빈 공간에 파티션을 만든다. **먼저 확인 명령으로 데이터가 없는지 본다.** +```bash +sudo sgdisk -p /dev/sda # 확인만: 1번(Microsoft reserved, 16MiB)만 있어야 한다 +sudo wipefs -n /dev/sda # 확인만: gpt/PMBR 외 서명이 없어야 한다 +sudo sgdisk -n 2:0:0 -t 2:8300 -c 2:innolive-logs /dev/sda +sudo partprobe /dev/sda +sudo mkfs.ext4 -L innolive-logs /dev/sda2 +sudo mkdir -p /srv/innolive-logs +echo 'LABEL=innolive-logs /srv/innolive-logs ext4 defaults,noatime,nofail 0 2' | sudo tee -a /etc/fstab +sudo systemctl daemon-reload # fstab 변경을 systemd에 알린다(RequiresMountsFor가 이 마운트를 본다) +sudo mount /srv/innolive-logs +sudo chown root:adm /srv/innolive-logs && sudo chmod 750 /srv/innolive-logs +``` +`nofail`이라 디스크가 고장 나도 부팅은 멈추지 않는다(그동안 보관만 실패한다). + +### 설치 (PR 머지 후, root) +1) 로컬에서 파일 네 개를 서버로 올린다. +```bash +ssh -p <계정>@<서버> 'mkdir -p ~/log-archive-install' +scp -P deploy/log-archive.sh deploy/apply-release.sh \ + deploy/innolive-log-archive.service deploy/innolive-log-archive.timer <계정>@<서버>:~/log-archive-install/ +``` +2) 서버에서 설치한다. +```bash +cd ~/log-archive-install +sudo install -o root -g root -m 755 log-archive.sh /opt/innolive/deploy/log-archive.sh +sudo install -o root -g root -m 755 -b apply-release.sh /opt/innolive/deploy/apply-release.sh +sudo install -o root -g root -m 644 innolive-log-archive.service innolive-log-archive.timer /etc/systemd/system/ +sudo systemctl daemon-reload +``` +3) 설치 전에 미리 떠 둔 보관본(`~hkit/innolive-log-archive-preinstall/`, 2026-09-23 생성)이 있으면 +**첫 실행 전에** 옮긴다. journald가 그사이 오래된 로그를 지웠어도 그 날짜가 보존된다. +기존 파일은 덮어쓰지 않고(`--update=none`), 이후 스크립트는 없는 날만 채우고 어제·오늘만 다시 쓴다. +```bash +sudo cp --update=none ~hkit/innolive-log-archive-preinstall/*.log.zst /srv/innolive-logs/ +sudo chgrp adm /srv/innolive-logs/*.log.zst && sudo chmod 640 /srv/innolive-logs/*.log.zst +``` +4) 첫 실행 후 매일 타이머를 켠다. +```bash +sudo systemctl start innolive-log-archive.service # 첫 실행: 과거 로그 채우기 +sudo systemctl enable --now innolive-log-archive.timer +``` + +### 확인·열람 +```bash +systemctl list-timers innolive-log-archive.timer +journalctl -u innolive-log-archive -n 5 +ls -l /srv/innolive-logs +zstdcat /srv/innolive-logs/innolive-server-2026-09-21.log.zst | less +zstdgrep 'peer_connection_failed' /srv/innolive-logs/innolive-server-*.log.zst +``` + ## GitHub 설정 - Secrets (Actions): `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN`(Read&Write), diff --git a/deploy/apply-release.sh b/deploy/apply-release.sh index 8b1c447..f4d9915 100755 --- a/deploy/apply-release.sh +++ b/deploy/apply-release.sh @@ -75,6 +75,10 @@ docker login docker.io -u "${DOCKERHUB_USER}" --password-stdin \ docker pull "${INNOLIVE_IMAGE}:${TAG}" >>"${DETAIL_LOG}" 2>&1 || fail "docker pull failed" echo "PULL OK" +# ── 재시작 전 런타임 로그 보관 ────────────────────────────────── +# 실패해도 배포는 진행한다 — 원본은 journald에 남는다. 출력은 상세 로그에만 쓴다. +/opt/innolive/deploy/log-archive.sh >>"${DETAIL_LOG}" 2>&1 || detail "log archive failed" + # ── 태그 교체(이전 태그는 롤백용으로 백업) ─────────────────────── if [[ -f "${TAG_FILE}" ]]; then cp "${TAG_FILE}" "${TAG_FILE}.prev" diff --git a/deploy/innolive-log-archive.service b/deploy/innolive-log-archive.service new file mode 100644 index 0000000..f95a896 --- /dev/null +++ b/deploy/innolive-log-archive.service @@ -0,0 +1,14 @@ +# 서버 런타임 로그를 날짜별 파일로 보관한다(log-archive.sh). innolive-log-archive.timer가 매일 실행한다. +# /etc/systemd/system/innolive-log-archive.service 로 설치한다. + +[Unit] +Description=Archive InnoLive server runtime logs to dated files +# 보관 디스크(/srv/innolive-logs)가 마운트된 뒤에만 돈다. 없으면 스크립트가 쓰지 않고 실패한다. +RequiresMountsFor=/srv/innolive-logs + +[Service] +Type=oneshot +ExecStart=/opt/innolive/deploy/log-archive.sh +# 방송 중에도 돌 수 있으므로 CPU·디스크 우선순위를 가장 낮춘다. +Nice=19 +IOSchedulingClass=idle diff --git a/deploy/innolive-log-archive.timer b/deploy/innolive-log-archive.timer new file mode 100644 index 0000000..144e59c --- /dev/null +++ b/deploy/innolive-log-archive.timer @@ -0,0 +1,12 @@ +# 매일 00:10에 전날 로그를 마감해 보관한다. /etc/systemd/system/innolive-log-archive.timer 로 설치한다. + +[Unit] +Description=Daily InnoLive runtime log archive + +[Timer] +OnCalendar=*-*-* 00:10:00 +# 서버가 꺼져 있어 놓친 실행은 켜진 뒤 한 번 돈다. +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/deploy/log-archive.sh b/deploy/log-archive.sh new file mode 100755 index 0000000..c786300 --- /dev/null +++ b/deploy/log-archive.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# 서버 런타임 로그(journald의 innolive-server)를 날짜별 파일로 보관한다. +# 사용법: log-archive.sh (root. 매일 innolive-log-archive.timer와 배포 직전 apply-release.sh가 호출) +# +# journald는 용량 상한에 닿으면 오래된 로그부터 지우므로, 보관 디스크에 하루 한 파일 +# (innolive-server-YYYY-MM-DD.log.zst, 서버 현지 날짜)로 옮겨 둔다. +# - 아직 파일이 없는 날은 채우고, 어제와 오늘은 매번 다시 쓴다(멱등). 첫 실행은 +# journald에 남은 가장 오래된 날부터 채운다. +# - 스트림 키·토큰·비밀번호는 가려서 쓴다. 사용자 IP(ICE 후보)는 장애 조사에 필요해 남긴다. +# - 출력은 요약 한 줄뿐이다. 로그 본문을 stdout으로 내보내지 않는다(배포 출력 계약). +set -euo pipefail + +ARCHIVE_DIR="${INNOLIVE_LOG_ARCHIVE_DIR:-/srv/innolive-logs}" +UNIT="innolive-server" + +# 보관 디스크가 빠진 상태에서 루트 디스크의 빈 마운트 지점에 쓰는 사고를 막는다. +if ! mountpoint -q "${ARCHIVE_DIR}"; then + echo "log-archive: ${ARCHIVE_DIR} is not a mount point; nothing written" >&2 + exit 1 +fi + +mask() { + sed -E \ + -e 's#(rtmps?://[^ "]*/)[^ "/]+#\1***MASKED***#g' \ + -e 's#((stream_?key|access_?token|refresh_?token|owner_?token|client_?secret|secret|password|authorization)"?[=:] ?"?)[A-Za-z0-9._~+/=-]{16,}#\1***MASKED***#Ig' \ + -e 's#(Bearer )[A-Za-z0-9._~+/=-]+#\1***MASKED***#g' +} + +# head가 먼저 끝나 journalctl이 SIGPIPE를 받는 것은 정상이다 — 값은 아래에서 검증한다. +first_day="$(journalctl -u "${UNIT}" -o short-iso --no-pager 2>/dev/null | head -1 | cut -c1-10 || true)" +if [[ ! "${first_day}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then + echo "log-archive: no journal entries for ${UNIT}" >&2 + exit 0 +fi + +# 중간에 실패하면 쓰다 만 임시 파일을 지운다. +tmp="" +trap 'rm -f "${tmp}"' EXIT + +today="$(date +%F)" +yesterday="$(date -d yesterday +%F)" +written=0 +day="${first_day}" +while [[ ! "${day}" > "${today}" ]]; do + next="$(date -d "${day} +1 day" +%F)" + out="${ARCHIVE_DIR}/${UNIT}-${day}.log.zst" + if [[ ! -e "${out}" || ! "${day}" < "${yesterday}" ]]; then + tmp="$(mktemp "${ARCHIVE_DIR}/.${UNIT}-${day}.XXXXXX")" + journalctl -q -u "${UNIT}" -o short-iso --no-pager --since "${day} 00:00:00" --until "${next} 00:00:00" \ + | mask | zstd -q -19 -f -o "${tmp}" + chgrp adm "${tmp}" + chmod 640 "${tmp}" + mv -f "${tmp}" "${out}" + written=$((written + 1)) + fi + day="${next}" +done + +echo "log-archive: ${written} day file(s) written to ${ARCHIVE_DIR} (${first_day}..${today})"