feat(logging): wire LOG_DIR through env and align systemd helper

- logger uses config/env for LOG_DIR and LOG_LEVEL (single source of truth)
- Document absolute LOG_DIR for wipe/redeploy (Jenkins) in env and .env.example
- setup-kord-user-log-file.sh reads LOG_DIR from KORD_HOME/.env and syncs
  StandardOutput/StandardError/ExecStartPre mkdir to the same path

Made-with: Cursor
This commit is contained in:
mineseo-kim 2026-04-09 10:00:58 +09:00
parent 5280f1987b
commit dbadd936ca
4 changed files with 66 additions and 20 deletions

View File

@ -9,5 +9,6 @@ DATABASE_URL="postgresql://kord:password@localhost:5432/kord_db?schema=public"
# Logging (log4js — file only under LOG_DIR, no console appender) # Logging (log4js — file only under LOG_DIR, no console appender)
# Levels: trace, debug, info, warn, error, fatal # Levels: trace, debug, info, warn, error, fatal
LOG_LEVEL=info LOG_LEVEL=info
# Daily log files; keep 7 rotated days (see logger.ts). Directory is created at startup if missing. # Log directory (kord.log + dated rotations). Relative = from process cwd; use an absolute path on servers
# if the deploy directory is wiped (e.g. Jenkins): LOG_DIR=/var/lib/kord/logs
LOG_DIR=logs LOG_DIR=logs

View File

@ -1,12 +1,35 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Run ON THE SERVER as the same user that runs kord (e.g. psa), after: ssh psa@server # Run ON THE SERVER as the same user that runs kord (e.g. psa), after: ssh psa@server
# Switches kord user service from journal-only to append logs under ~/kord/logs/kord.log # Switches kord user service from journal-only to append stdout/stderr under LOG_DIR/kord.log
# LOG_DIR is read from $KORD_HOME/.env (LOG_DIR=...) when present, else $KORD_HOME/logs.
set -euo pipefail set -euo pipefail
KORD_HOME="${KORD_HOME:-$HOME/kord}"
ENV_FILE="${KORD_ENV_FILE:-$KORD_HOME/.env}"
UNIT="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/kord.service" UNIT="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/kord.service"
LOG_DIR="$HOME/kord/logs"
LOG_FILE="$LOG_DIR/kord.log" # Last LOG_DIR= line from .env; strip quotes and ~ ; relative paths are under KORD_HOME
resolve_log_dir() {
local default="${KORD_HOME}/logs" line raw
[[ -f "$ENV_FILE" ]] || { echo "$default"; return; }
line="$(grep -E '^[[:space:]]*LOG_DIR[[:space:]]*=' "$ENV_FILE" | tail -n1 || true)"
[[ -z "$line" ]] && { echo "$default"; return; }
raw="${line#*=}"
raw="$(printf '%s' "$raw" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e $'s/\r$//')"
if [[ "$raw" =~ ^\".*\"$ ]]; then raw="${raw#\"}"; raw="${raw%\"}"; fi
if [[ "$raw" =~ ^\'.*\'$ ]]; then raw="${raw#\'}"; raw="${raw%\'}"; fi
raw="${raw//\~/$HOME}"
[[ -z "$raw" ]] && { echo "$default"; return; }
if [[ "$raw" = /* ]]; then
echo "$raw"
else
echo "${KORD_HOME}/${raw#./}"
fi
}
LOG_DIR="$(resolve_log_dir)"
LOG_FILE="${LOG_DIR}/kord.log"
mkdir -p "$LOG_DIR" mkdir -p "$LOG_DIR"
@ -17,17 +40,28 @@ fi
cp -a "$UNIT" "${UNIT}.bak.$(date +%Y%m%d%H%M%S)" cp -a "$UNIT" "${UNIT}.bak.$(date +%Y%m%d%H%M%S)"
# Replace journal with file append (idempotent if already set) # Point journal or any previous append paths at the log file derived from .env LOG_DIR
sed -i \ sed -i \
-e 's|^StandardOutput=journal|StandardOutput=append:'"$LOG_FILE"'|' \ -e "s|^StandardOutput=journal|StandardOutput=append:${LOG_FILE}|" \
-e 's|^StandardError=journal|StandardError=append:'"$LOG_FILE"'|' \ -e "s|^StandardError=journal|StandardError=append:${LOG_FILE}|" \
"$UNIT"
sed -i \
-e "s|^StandardOutput=append:.*|StandardOutput=append:${LOG_FILE}|" \
-e "s|^StandardError=append:.*|StandardError=append:${LOG_FILE}|" \
"$UNIT" "$UNIT"
# systemd opens StandardOutput=append before ExecStart; if ~/kord/logs was deleted, # systemd opens StandardOutput=append before ExecStart; missing parent dir → status 209/STDOUT
# the service fails with (code=exited, status=209/STDOUT). Ensure the dir exists first. sed -i '/^ExecStartPre=-\/usr\/bin\/mkdir -p /d' "$UNIT"
if ! grep -qE '^ExecStartPre=.*mkdir.*kord/logs' "$UNIT"; then tmp="$(mktemp)"
sed -i '/^ExecStart=/i ExecStartPre=-/usr/bin/mkdir -p %h/kord/logs' "$UNIT" inserted=0
while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^ExecStart= ]] && [[ "$inserted" -eq 0 ]]; then
printf '%s\n' "ExecStartPre=-/usr/bin/mkdir -p ${LOG_DIR}"
inserted=1
fi fi
printf '%s\n' "$line"
done <"$UNIT" >"$tmp"
mv "$tmp" "$UNIT"
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user restart kord systemctl --user restart kord
@ -43,4 +77,5 @@ else
fi fi
echo echo
echo "LOG_DIR=$LOG_DIR"
echo "Follow logs: tail -f $LOG_FILE" echo "Follow logs: tail -f $LOG_FILE"

View File

@ -18,7 +18,11 @@ export const env = {
VOICE_CATEGORY_ID: process.env.VOICE_CATEGORY_ID || '', VOICE_CATEGORY_ID: process.env.VOICE_CATEGORY_ID || '',
/** log4js level: trace | debug | info | warn | error | fatal */ /** log4js level: trace | debug | info | warn | error | fatal */
LOG_LEVEL: process.env.LOG_LEVEL || 'info', LOG_LEVEL: process.env.LOG_LEVEL || 'info',
/** Directory for rotated kord.log (see src/utils/logger.ts) */ /**
* Directory for log4js `kord.log` (created at startup). Relative paths resolve from `process.cwd()`.
* For Jenkins or wipe-and-redeploy flows, set an absolute path **outside** the deploy tree (e.g. `/var/lib/kord/logs`)
* so logs survive redeploys and match `StandardOutput=append` in systemd if you point it at the same file.
*/
LOG_DIR: process.env.LOG_DIR || 'logs', LOG_DIR: process.env.LOG_DIR || 'logs',
INSTANCE_ID: generateInstanceId(), INSTANCE_ID: generateInstanceId(),
}; };

View File

@ -1,19 +1,25 @@
import { mkdirSync } from 'fs'; import { mkdirSync } from 'fs';
import { config } from 'dotenv';
import log4js from 'log4js'; import log4js from 'log4js';
import { resolve } from 'path'; import { resolve } from 'path';
import { env } from '../config/env';
// Load .env before reading LOG_LEVEL / LOG_DIR (same rule as config/env.ts).
config({ path: process.env.DOTENV_CONFIG_PATH || resolve(process.cwd(), '.env') });
const LOG_LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'] as const; const LOG_LEVELS = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'] as const;
type LogLevel = (typeof LOG_LEVELS)[number]; type LogLevel = (typeof LOG_LEVELS)[number];
function resolveLogLevel(): LogLevel { function resolveLogLevel(): LogLevel {
const raw = (process.env.LOG_LEVEL || 'info').toLowerCase(); const raw = env.LOG_LEVEL.toLowerCase();
return (LOG_LEVELS as readonly string[]).includes(raw) ? (raw as LogLevel) : 'info'; return (LOG_LEVELS as readonly string[]).includes(raw) ? (raw as LogLevel) : 'info';
} }
/** Resolves LOG_DIR from .env: absolute paths unchanged; relative paths from cwd. */
function resolveLogDir(raw: string): string {
const trimmed = raw.trim();
if (!trimmed) {
return resolve('logs');
}
return resolve(trimmed);
}
function ensureLogDir(dir: string): void { function ensureLogDir(dir: string): void {
try { try {
mkdirSync(dir, { recursive: true }); mkdirSync(dir, { recursive: true });
@ -24,7 +30,7 @@ function ensureLogDir(dir: string): void {
} }
} }
const logDir = resolve((process.env.LOG_DIR || 'logs').trim()); const logDir = resolveLogDir(env.LOG_DIR);
const level = resolveLogLevel(); const level = resolveLogLevel();
ensureLogDir(logDir); ensureLogDir(logDir);