From 3cceb366fdcc5ab2338400ec71750a56a8737abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EC=A0=95=EB=B9=88?= Date: Wed, 23 Sep 2026 10:18:48 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EC=84=9C=EB=B2=84=20=EB=9F=B0?= =?UTF-8?q?=ED=83=80=EC=9E=84=20=EB=A1=9C=EA=B7=B8=EB=A5=BC=20=EB=A7=A4?= =?UTF-8?q?=EC=9D=BC=20=EB=82=A0=EC=A7=9C=EB=B3=84=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=B4=EA=B4=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- deploy/README.md | 46 ++++++++++++++++++++++ deploy/innolive-log-archive.service | 14 +++++++ deploy/innolive-log-archive.timer | 12 ++++++ deploy/log-archive.sh | 59 +++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+) create mode 100644 deploy/innolive-log-archive.service create mode 100644 deploy/innolive-log-archive.timer create mode 100755 deploy/log-archive.sh diff --git a/deploy/README.md b/deploy/README.md index eac95e7..9d7e096 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -60,6 +60,52 @@ 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 mount /srv/innolive-logs +sudo chown root:adm /srv/innolive-logs && sudo chmod 750 /srv/innolive-logs +``` +`nofail`이라 디스크가 고장 나도 부팅은 멈추지 않는다(그동안 보관만 실패한다). + +### 설치 (PR 머지 후, root) +```bash +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 +sudo systemctl enable --now innolive-log-archive.timer +sudo systemctl start innolive-log-archive.service # 첫 실행: 과거 로그 채우기 +``` + +### 확인·열람 +```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/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})" From 20c9649cfc2285ed5ecacb74df670dacd5928885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EC=A0=95=EB=B9=88?= Date: Wed, 23 Sep 2026 10:18:48 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EB=B0=B0=ED=8F=AC=20=EC=9E=AC?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EC=A7=81=EC=A0=84=EC=97=90=20=EB=9F=B0?= =?UTF-8?q?=ED=83=80=EC=9E=84=20=EB=A1=9C=EA=B7=B8=EB=A5=BC=20=EB=B3=B4?= =?UTF-8?q?=EA=B4=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- deploy/apply-release.sh | 4 ++++ 1 file changed, 4 insertions(+) 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" From e95bc867d290b6485fbb961880b1d0867b063748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EC=A0=95=EB=B9=88?= Date: Wed, 23 Sep 2026 10:23:16 +0900 Subject: [PATCH 3/5] =?UTF-8?q?docs:=20=EC=84=A4=EC=B9=98=20=EC=A0=84=20?= =?UTF-8?q?=EB=B3=B4=EA=B4=80=EB=B3=B8=EC=9D=84=20=EB=B3=B4=EA=B4=80=20?= =?UTF-8?q?=EB=94=94=EC=8A=A4=ED=81=AC=EB=A1=9C=20=EB=A8=BC=EC=A0=80=20?= =?UTF-8?q?=EC=98=AE=EA=B8=B0=EB=8A=94=20=EC=A0=88=EC=B0=A8=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- deploy/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/deploy/README.md b/deploy/README.md index 9d7e096..d7ed747 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -96,6 +96,13 @@ sudo systemctl daemon-reload sudo systemctl enable --now innolive-log-archive.timer sudo systemctl start innolive-log-archive.service # 첫 실행: 과거 로그 채우기 ``` +설치 전에 미리 떠 둔 보관본(`~hkit/innolive-log-archive-preinstall/`, 2026-09-23 생성)이 있으면 +**첫 실행 전에** 먼저 옮긴다. journald가 그사이 오래된 로그를 지웠어도 그 날짜가 보존된다. +기존 파일은 덮어쓰지 않고(`-n`), 스크립트는 없는 날만 채우고 어제·오늘만 다시 쓴다. +```bash +sudo cp -n ~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 +``` ### 확인·열람 ```bash From f9f3db60fcc25e093221c36cb21184a4ddb69e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EC=A0=95=EB=B9=88?= Date: Wed, 23 Sep 2026 10:28:15 +0900 Subject: [PATCH 4/5] =?UTF-8?q?docs:=20=EB=A1=9C=EA=B7=B8=20=EB=B3=B4?= =?UTF-8?q?=EA=B4=80=20=EC=84=A4=EC=B9=98=20=EC=A0=88=EC=B0=A8=EC=97=90=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=C2=B7fstab=20=EB=B0=98=EC=98=81=20?= =?UTF-8?q?=EB=8B=A8=EA=B3=84=EB=A5=BC=20=EB=84=A3=EA=B3=A0=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=EB=A5=BC=20=EB=B0=94=EB=A1=9C=EC=9E=A1=EB=8A=94?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- deploy/README.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/deploy/README.md b/deploy/README.md index d7ed747..78082a5 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -82,27 +82,39 @@ 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 -sudo systemctl enable --now innolive-log-archive.timer -sudo systemctl start innolive-log-archive.service # 첫 실행: 과거 로그 채우기 ``` -설치 전에 미리 떠 둔 보관본(`~hkit/innolive-log-archive-preinstall/`, 2026-09-23 생성)이 있으면 -**첫 실행 전에** 먼저 옮긴다. journald가 그사이 오래된 로그를 지웠어도 그 날짜가 보존된다. -기존 파일은 덮어쓰지 않고(`-n`), 스크립트는 없는 날만 채우고 어제·오늘만 다시 쓴다. +3) 설치 전에 미리 떠 둔 보관본(`~hkit/innolive-log-archive-preinstall/`, 2026-09-23 생성)이 있으면 +**첫 실행 전에** 옮긴다. journald가 그사이 오래된 로그를 지웠어도 그 날짜가 보존된다. +기존 파일은 덮어쓰지 않고(`-n`), 이후 스크립트는 없는 날만 채우고 어제·오늘만 다시 쓴다. ```bash sudo cp -n ~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 From f36bb2c972d8d690b1eecfd94523e34506dacf47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EC=A0=95=EB=B9=88?= Date: Wed, 23 Sep 2026 10:35:38 +0900 Subject: [PATCH 5/5] =?UTF-8?q?docs:=20=EB=B3=B4=EA=B4=80=EB=B3=B8=20?= =?UTF-8?q?=EB=B3=B5=EC=82=AC=EB=A5=BC=20cp=20--update=3Dnone=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=94=EA=BE=BC=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- deploy/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/README.md b/deploy/README.md index 78082a5..75788d6 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -105,9 +105,9 @@ sudo systemctl daemon-reload ``` 3) 설치 전에 미리 떠 둔 보관본(`~hkit/innolive-log-archive-preinstall/`, 2026-09-23 생성)이 있으면 **첫 실행 전에** 옮긴다. journald가 그사이 오래된 로그를 지웠어도 그 날짜가 보존된다. -기존 파일은 덮어쓰지 않고(`-n`), 이후 스크립트는 없는 날만 채우고 어제·오늘만 다시 쓴다. +기존 파일은 덮어쓰지 않고(`--update=none`), 이후 스크립트는 없는 날만 채우고 어제·오늘만 다시 쓴다. ```bash -sudo cp -n ~hkit/innolive-log-archive-preinstall/*.log.zst /srv/innolive-logs/ +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) 첫 실행 후 매일 타이머를 켠다.