#!/usr/bin/env bash # # daily_tibero_check. - 원격 서버 로그에서 기간/키워드로 추출해 로컬에 저장 # # 사용법: # ./daily_tibero_check. # 어제 하루 (기본값) # ./daily_tibero_check. 2026-10-01 # 특정 하루 # ./daily_tibero_check. 2026-10-01 2026-10-05 # 기간 (시작일~종료일, 양 끝 포함) # # 아래 설정값은 실행 시 환경변수로 덮어쓸 수도 있습니다. # REMOTE_HOST=10.0.0.5 KEYWORDS="ERROR,FATAL" ./daily_tibero_check. # OUTPUT_DIR=/data/check ./daily_tibero_check. # → /data/check/10.0.0.5_2026-10-05_check.log # DATE_FORMAT="YYYY/MM/DD hh:mm:ss" ./daily_tibero_check. # SSH_PASSWORD='비밀번호' ./daily_tibero_check. # # 종료코드: 0 정상 / 1 실행 오류 / 2 대상 파일에 해당 기간 로그가 없어 확인 불가 # 실패 시: 결과 파일 대신 OUTPUT_DIR에 IP_날짜_check_fail.log 를 만들고 에러 내용을 기록 # ============================ 설정 ============================ REMOTE_HOST="${REMOTE_HOST:-10.14.85.86}" # 접속할 IP REMOTE_USER="tibero" # 접속 유저 # 접속 비밀번호: 작은따옴표 안에 그대로 적으세요 ($, ", ! 등 특수문자 그대로 가능) # 비밀번호에 작은따옴표(')가 있으면 '\'' 로 바꿔 적으세요. # 비워 두면 기존처럼 SSH 키 또는 직접 입력으로 접속합니다. DEFAULT_SSH_PASSWORD='tibero' SSH_PASSWORD="${SSH_PASSWORD:-$DEFAULT_SSH_PASSWORD}" KEYWORDS="${KEYWORDS:-failed,shutdown}" # 키워드 (쉼표 구분, 하나라도 포함되면 추출) # 로그 경로: 절대경로로 지정 (* 사용 가능, .gz도 읽음) # 실제 경로는 서버에서 echo $TB_HOME/instance/$TB_SID/log/slog/sys.log 로 확인 REMOTE_LOG_PATH="${REMOTE_LOG_PATH:-/home/tibero/tibero7/instance/tibero/log/slog/sys.log}" # 로그의 시간 형식 (대소문자 구분) # YYYY 연도 MM 월(01~12) Mon 영문 월(Jan~Dec) DD 일 # hh 시 mm 분 ss 초 SSS 밀리초 그 외 문자는 그대로 일치 # 예) 2026-09-04T16:04:40.048 → YYYY-MM-DDThh:mm:ss.SSS # 2026/09/04 16:04:40 → YYYY/MM/DD hh:mm:ss # 04-Sep-2026 16:04:40 → DD-Mon-YYYY hh:mm:ss DATE_FORMAT="${DATE_FORMAT:-YYYY-MM-DDThh:mm:ss.SSS}" OUTPUT_DIR="${OUTPUT_DIR:-./daily_tibero_check_logs}" # 결과 저장 디렉터리 (파일명은 IP_날짜_check.log, 실패 시 IP_날짜_check_fail.log 로 자동 생성) SSH_PORT="${SSH_PORT:-22}" SSH_KEY="${SSH_KEY:-}" # 키 파일 경로 (비우면 ssh 기본 설정 사용) IGNORE_CASE="${IGNORE_CASE:-0}" # 1이면 대소문자 무시 # ============================================================== set -u FAIL_FILE="" # 출력 디렉터리 준비 후 설정됨 (그 전 오류는 화면에만 출력) ERR_LOG="" # ssh/원격 쪽 에러 메시지를 받아 두는 임시 파일 # 실패 기록 파일 작성: 실행 정보 + 원격 에러 메시지 + 추가 메시지 write_fail() { [ -n "$FAIL_FILE" ] || return 0 { echo "실행시각 : $(date '+%Y-%m-%d %H:%M:%S')" echo "결과 : 실패" echo "서버 : ${REMOTE_USER}@${REMOTE_HOST}:${SSH_PORT}" echo "로그 : ${REMOTE_LOG_PATH}" echo "기간 : ${START_DATE} ~ ${END_DATE}" echo "키워드 : ${KEYWORDS}" echo "------------------------------------------------------------" if [ -n "$ERR_LOG" ] && [ -s "$ERR_LOG" ]; then tr -d '\r' < "$ERR_LOG"; fi if [ $# -gt 0 ]; then printf '%s\n' "$@"; fi } > "$FAIL_FILE" 2>/dev/null \ && echo "실패기록 : ${FAIL_FILE}" >&2 \ || echo "[WARN] 실패 기록 파일을 쓸 수 없습니다: ${FAIL_FILE}" >&2 } die() { echo "[ERROR] $*" >&2; write_fail "[ERROR] $*"; exit 1; } warn() { echo "[WARN] $*" >&2; } # 종료 시 임시 파일 정리 CLEANUP_FILES=() cleanup() { [ ${#CLEANUP_FILES[@]} -gt 0 ] && rm -f "${CLEANUP_FILES[@]}"; } trap cleanup EXIT # 어제 날짜 (Linux GNU date / macOS BSD date 모두 지원) yesterday() { if date --version >/dev/null 2>&1; then date -d yesterday +%F else date -v-1d +%F fi } is_date() { [[ "$1" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; } # DATE_FORMAT → awk 정규식 + 연/월/일 위치(1부터) 계산 # 결과: DATE_RE, Y_POS, M_POS, D_POS, MON_TEXT(1이면 영문 월) build_date_regex() { local fmt="$1" pos=1 c DATE_RE=""; Y_POS=0; M_POS=0; D_POS=0; MON_TEXT=0 while [ -n "$fmt" ]; do case "$fmt" in YYYY*) DATE_RE+="[0-9][0-9][0-9][0-9]"; Y_POS=$pos; pos=$((pos+4)); fmt="${fmt:4}" ;; SSS*) DATE_RE+="[0-9][0-9][0-9]"; pos=$((pos+3)); fmt="${fmt:3}" ;; Mon*) DATE_RE+="(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)" M_POS=$pos; MON_TEXT=1; pos=$((pos+3)); fmt="${fmt:3}" ;; MM*) DATE_RE+="[0-9][0-9]"; M_POS=$pos; pos=$((pos+2)); fmt="${fmt:2}" ;; DD*) DATE_RE+="[0-9][0-9]"; D_POS=$pos; pos=$((pos+2)); fmt="${fmt:2}" ;; hh*|mm*|ss*) DATE_RE+="[0-9][0-9]"; pos=$((pos+2)); fmt="${fmt:2}" ;; *) c="${fmt:0:1}" case "$c" in [A-Za-z0-9:,\ -]) DATE_RE+="$c" ;; '^'|'\') die "DATE_FORMAT에 사용할 수 없는 문자입니다: $c" ;; *) DATE_RE+="[$c]" ;; # . [ ] / ( ) 등은 문자 그대로 esac pos=$((pos+1)); fmt="${fmt:1}" ;; esac done [ "$Y_POS" -gt 0 ] && [ "$M_POS" -gt 0 ] && [ "$D_POS" -gt 0 ] \ || die "DATE_FORMAT에는 YYYY, MM(또는 Mon), DD가 모두 있어야 합니다: $1" } # ---------- 검색 기간 ---------- case $# in 0) START_DATE="$(yesterday)"; END_DATE="$START_DATE" ;; 1) START_DATE="$1"; END_DATE="$1" ;; 2) START_DATE="$1"; END_DATE="$2" ;; *) die "사용법: $0 [시작일 YYYY-MM-DD] [종료일 YYYY-MM-DD]" ;; esac is_date "$START_DATE" && is_date "$END_DATE" || die "날짜 형식은 YYYY-MM-DD 입니다." [[ "$START_DATE" > "$END_DATE" ]] && die "시작일($START_DATE)이 종료일($END_DATE)보다 늦습니다." # ---------- 출력 파일 ---------- OUTPUT_DIR="${OUTPUT_DIR%/}" if [ "$START_DATE" = "$END_DATE" ]; then OUT_BASE="${OUTPUT_DIR}/${REMOTE_HOST}_${START_DATE}_check" else OUT_BASE="${OUTPUT_DIR}/${REMOTE_HOST}_${START_DATE}_${END_DATE}_check" fi OUT_FILE="${OUT_BASE}.log" mkdir -p "$OUTPUT_DIR" || die "출력 디렉터리를 만들 수 없습니다: $OUTPUT_DIR" FAIL_FILE="${OUT_BASE}_fail.log" # 여기부터의 오류는 실패 파일에도 기록 # ---------- 로그 경로 / 시간 형식 확인 ---------- [[ "$REMOTE_LOG_PATH" == /* ]] || die "REMOTE_LOG_PATH는 절대경로(/로 시작)로 지정하세요: $REMOTE_LOG_PATH" [ -n "$DATE_FORMAT" ] || die "DATE_FORMAT이 비어 있습니다." build_date_regex "$DATE_FORMAT" # ---------- 키워드 파싱 (쉼표 구분, 앞뒤 공백 제거) ---------- [ -n "$KEYWORDS" ] || die "KEYWORDS가 비어 있습니다." IFS=',' read -r -a RAW_KW <<< "$KEYWORDS" KW=() for k in "${RAW_KW[@]}"; do k="${k#"${k%%[![:space:]]*}"}" k="${k%"${k##*[![:space:]]}"}" [ -n "$k" ] && KW+=("$k") done [ ${#KW[@]} -gt 0 ] || die "유효한 키워드가 없습니다." # ---------- 임시 파일 ---------- TMP_FILE="$(mktemp "${OUT_FILE}.XXXXXX")" || die "임시 파일 생성 실패" ERR_LOG="$(mktemp "${OUTPUT_DIR}/.err.XXXXXX")" || die "임시 파일 생성 실패" CLEANUP_FILES+=("$TMP_FILE" "$ERR_LOG") # ---------- SSH 옵션 / 인증 ---------- SSH_OPTS=(-p "$SSH_PORT" -o ConnectTimeout=10) [ -n "$SSH_KEY" ] && SSH_OPTS+=(-i "$SSH_KEY") SSH_CMD=(ssh) AUTH_DESC="SSH 키 또는 직접 입력" if [ -n "$SSH_PASSWORD" ]; then # 처음 보는 서버의 호스트키 확인 질문에 비밀번호가 대신 입력되는 것을 막음 # → 처음 접속하는 서버는 수동으로 한 번 접속해 호스트키를 등록해 두어야 함 SSH_OPTS+=(-o StrictHostKeyChecking=yes) if command -v sshpass >/dev/null 2>&1; then # sshpass -e : 비밀번호를 환경변수(SSHPASS)로 넘겨 ps 명령에 노출되지 않게 함 export SSHPASS="$SSH_PASSWORD" SSH_CMD=(sshpass -e ssh) AUTH_DESC="비밀번호 자동 입력 (sshpass)" else # sshpass가 없으면 OpenSSH 8.4 이상의 SSH_ASKPASS_REQUIRE=force 사용 SSH_VER="$(ssh -V 2>&1)" read -r V_MAJOR V_MINOR <<< "$(printf '%s' "$SSH_VER" | sed -n 's/^OpenSSH_\([0-9]*\)\.\([0-9]*\).*/\1 \2/p')" if [ "${V_MAJOR:-0}" -lt 8 ] || { [ "${V_MAJOR:-0}" -eq 8 ] && [ "${V_MINOR:-0}" -lt 4 ]; }; then die "비밀번호 자동 입력에는 sshpass 설치 또는 OpenSSH 8.4 이상이 필요합니다. (현재: $SSH_VER)" fi # 비밀번호는 파일에 쓰지 않고, 환경변수를 출력만 하는 askpass 스크립트를 임시로 생성 ASKPASS_FILE="$(mktemp "${OUTPUT_DIR}/.askpass.XXXXXX")" || die "임시 파일 생성 실패" CLEANUP_FILES+=("$ASKPASS_FILE") cat > "$ASKPASS_FILE" <<'EOF' #!/bin/sh printf '%s\n' "$FETCH_LOG_SSH_PASSWORD" EOF chmod 700 "$ASKPASS_FILE" export FETCH_LOG_SSH_PASSWORD="$SSH_PASSWORD" SSH_ASKPASS="$ASKPASS_FILE" SSH_ASKPASS_REQUIRE=force SSH_OPTS+=(-o NumberOfPasswordPrompts=1) AUTH_DESC="비밀번호 자동 입력 (SSH_ASKPASS)" fi fi echo "서버 : ${REMOTE_USER}@${REMOTE_HOST}:${SSH_PORT}" echo "인증 : ${AUTH_DESC}" echo "로그 : ${REMOTE_LOG_PATH}" echo "시간형식 : ${DATE_FORMAT}" echo "기간 : ${START_DATE} ~ ${END_DATE}" echo "키워드 : $(IFS='|'; echo "${KW[*]}")" # ---------- 원격 실행 ---------- # 인자는 printf %q로 안전하게 감싸 원격 bash에 전달 (공백/특수문자/한글 키워드 대응) { printf 'set -- ' printf '%q ' "$START_DATE" "$END_DATE" "$REMOTE_LOG_PATH" "$IGNORE_CASE" \ "$DATE_FORMAT" "$DATE_RE" "$Y_POS" "$M_POS" "$D_POS" "$MON_TEXT" "${KW[@]}" printf '\n' cat <<'REMOTE_SCRIPT' start="$1"; end="$2"; log_path="$3"; ignore_case="$4" date_format="$5"; date_re="$6"; y_pos="$7"; m_pos="$8"; d_pos="$9"; mon_text="${10}" shift 10 shopt -s nullglob files=() for f in $log_path; do [ -f "$f" ] && files+=("$f"); done if [ ${#files[@]} -eq 0 ]; then echo "로그 파일을 찾을 수 없습니다: $log_path" >&2 exit 13 fi grep_opts=(-F) [ "$ignore_case" = "1" ] && grep_opts+=(-i) for kw in "$@"; do grep_opts+=(-e "$kw"); done # awk가 집계한 "기간 내 줄 수 / 파일의 최초 날짜 / 최종 날짜"를 받을 임시 파일 stat_file="$(mktemp)" || exit 1 trap 'rm -f "$stat_file"' EXIT # zcat -f : 일반 파일/압축 파일 모두 읽음 # awk : DATE_FORMAT에 맞는 시간을 찾아 날짜로 기간 필터 + 집계 # grep -F : 키워드를 정규식이 아닌 문자열 그대로 매칭 (OR 조건) # sort : 여러 파일을 읽을 때 시간순으로 정렬 zcat -f -- "${files[@]}" \ | DATE_RE="$date_re" awk -v s="$start" -v e="$end" -v sf="$stat_file" \ -v yp="$y_pos" -v mp="$m_pos" -v dp="$d_pos" -v mt="$mon_text" ' BEGIN { re = ENVIRON["DATE_RE"]; months = "JanFebMarAprMayJunJulAugSepOctNovDec" } match($0, re) { y = substr($0, RSTART + yp - 1, 4) if (mt == 1) m = sprintf("%02d", (index(months, substr($0, RSTART + mp - 1, 3)) + 2) / 3) else m = substr($0, RSTART + mp - 1, 2) d = y "-" m "-" substr($0, RSTART + dp - 1, 2) if (min == "" || d < min) min = d if (d > max) max = d if (d >= s && d <= e) { n++; print } } END { printf "%d %s %s\n", n, min, max > sf }' \ | grep "${grep_opts[@]}" \ | LC_ALL=C sort st=("${PIPESTATUS[@]}") read -r in_range fmin fmax < "$stat_file" # 대상 파일에 해당 기간 로그가 한 줄도 없으면 확인 불가로 처리 if [ "${in_range:-0}" -eq 0 ]; then { echo "[ERROR] ${start} ~ ${end} 구간의 메시지 확인이 불가능합니다." echo " 대상 파일에 해당 날짜의 로그가 없습니다: ${files[*]}" if [ -n "$fmin" ]; then echo " (대상 파일에 기록된 기간: ${fmin} ~ ${fmax})" else echo " (대상 파일에서 DATE_FORMAT(${date_format}) 형식의 시간을 찾을 수 없습니다)" fi } >&2 exit 16 fi [ "${st[0]}" -eq 1 ] && exit 14 exit 0 REMOTE_SCRIPT } | "${SSH_CMD[@]}" "${SSH_OPTS[@]}" "${REMOTE_USER}@${REMOTE_HOST}" 'bash -s' > "$TMP_FILE" 2> "$ERR_LOG" RC=$? # ssh/원격에서 나온 메시지를 화면에 출력 (실패 시 실패 파일에도 함께 기록됨) [ -s "$ERR_LOG" ] && tr -d '\r' < "$ERR_LOG" >&2 # 5~7은 sshpass 전용 종료코드, 13~16은 원격 스크립트 종료코드 case $RC in 0) ;; 5) die "비밀번호가 맞지 않습니다: ${REMOTE_USER}@${REMOTE_HOST}" ;; 6) die "호스트키가 등록되지 않은 서버입니다. 먼저 'ssh -p ${SSH_PORT} ${REMOTE_USER}@${REMOTE_HOST}'로 한 번 접속해 yes로 등록하세요." ;; 7) die "서버 호스트키가 변경되었습니다. 원인 확인 후 'ssh-keygen -R ${REMOTE_HOST}'로 정리하고 다시 등록하세요." ;; 13) die "원격 서버에 로그 파일이 없습니다: $REMOTE_LOG_PATH" ;; 14) warn "일부 로그 파일을 읽는 중 오류가 있었습니다. 읽은 부분만 저장합니다." ;; 16) write_fail; exit 2 ;; # 해당 기간 로그 없음 (원격 에러 메시지를 실패 파일에 기록) 255) if [ -n "$SSH_PASSWORD" ]; then die "SSH 접속 실패: ${REMOTE_USER}@${REMOTE_HOST}:${SSH_PORT} (비밀번호, 호스트키 등록 여부, 네트워크를 확인하세요. 처음 접속하는 서버라면 'ssh -p ${SSH_PORT} ${REMOTE_USER}@${REMOTE_HOST}'로 한 번 접속해 yes로 등록하세요)" else die "SSH 접속 실패: ${REMOTE_USER}@${REMOTE_HOST}:${SSH_PORT}" fi ;; *) die "원격 명령 실행 실패 (exit $RC)" ;; esac mv "$TMP_FILE" "$OUT_FILE" || die "결과 파일 저장 실패: $OUT_FILE" rm -f "$FAIL_FILE" # 같은 날짜로 재실행해 성공했으면 이전 실패 기록은 삭제 LINES=$(wc -l < "$OUT_FILE" | tr -d ' ') if [ "$LINES" -eq 0 ]; then echo "완료 : 해당 기간에 키워드 로그 없음 (빈 파일 생성: $OUT_FILE)" else echo "완료 : ${LINES}줄 → ${OUT_FILE}" fi |