웹 서버 자동 점검
소개
: 웹 홈페이지의 출력 상태 및 서버 상태를 수시로 체크하여 오류 보고 메일을 전송하는 프로젝트
프로젝트 개요
: 홈페이지가 종종 서버가 다운되거나 서버가 살아있음에도 페이지 출력이 안되는 경우가 있어 수시로 확인해줘야했다. 따라서 이를 매시간마다 자동으로 점검하여 오류 발생 시 이메일로 전송하는 애플리케이션을 개발하고자한다.
기술적 세부 사항
- SpringBoot 3.3.5
- JAVA 17
- JSOUP
- JSCH
- SpringBoot-stater-mail
주요 특징
: 홈페이지의 HTML 출력 상태, HTTP Status, Server Status, processChild 를 Jsoup(HTML Parser)와 JSCH(SSH 접속) 라이브러리를 사용하여 점검 후 발생한 에러를 전역으로 수집하여 메일링하는 시스템
직면 한 과제
: 개발 과정에서 겪었던 구체적인 과제 와 이를 어떻게 극복했는지 공유하세요 .
코드 스니펫
: 관련 코드 포함핵심 요점이나 해결책을 설명하는 스니펫 . 복잡한 논리를 설명하는 데 주석을 사용하세요 .
결과 및 성과
: 프로젝트 결과를 포함하여 다음을 논의하십시오.
측정항목이나 받은 피드백. 결론 : 프로젝트 에서 배운 내용 과 다른 사람들에게 어떤 도움이 될 수 있는지 요약합니다 . 독자의 피드백이나 질문을 장려합니다 .
Jsoup 라이브러리
:HTML parser 라이브러리로, 다음 기능을 제공
- URL, 파일 또는 문자열에서 HTML 구문 분석
- DOM 트래버설 또는 CSS 선택기를 사용하여 데이터 찾기 및 추출
- HTML 요소, 속성 및 텍스트를 조작합니다.
- XSS를 방지하기 위해 안전 목록에 대해 사용자 제출 콘텐츠를 정리합니다.
- 깔끔한 HTML 출력
다운로드 및 Gradle
implementation 'org.jsoup:jsoup:1.17.2'
Jsoup 사용법
- Document - Connect 이후 받은 HTML 전체 문서
- Elements - Element가 모인 자료형
- Element - Document의 HTML 요소
Jsoup 예제
public class JsoupExample {
public static void main(String[] args) {
// 타겟 URL
String url = "https://www.example.com";
try {
// URL에 연결하고, HTML 문서를 가져온다.
Document doc = Jsoup.connect(url).get();
// 문서 제목 추출 및 출력
String title = doc.title();
System.out.println("Title: " + title);
} catch (IOException e) {
System.err.println("Error fetching the URL: " + e.getMessage());
}
}
}
적용
package com.solomonm.HPChecker.manager;
import com.solomonm.HPChecker.config.ErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* 지정된 URL의 웹 페이지에 접속하여 상태 코드와 HTML 타이틀을 확인하는 클래스
*
* @author coffebara
* @version 1.0
*/
@Slf4j
@Component
public class WebChecker {
private final static String URL = "naver.com"; // 홈페이지 URL
private final static String WEB_TITLE = "naver";
/**
* Jsoup Connection 객체를 생성하고 설정하는 메서드
*
* @return 설정된 Jsoup Connection 객체
*/
public Connection getConnection() {
Connection connection = Jsoup.connect(URL)
.userAgent("Mozilla")
.timeout(3000);
return connection;
}
/**
* 웹페이지의 HTML title 을 확인하는 메서드
*
* @param connection Jsoup Connection 객체
* @throws IOException 연결 오류 발생
*/
public void checkHTMLTitle(Connection connection) throws IOException {
// HTML의 title 가져오기
String title = connection.get().title();
log.info("title: {}", title);
if (!title.equalsIgnoreCase(WEB_TITLE)) {
log.info("=== HTML 점검 성공 ===");
} else {
log.error("=== 웹 페이지 오류 ===");
GlobalErrorManager.getInstance().addError(ErrorCode.HTML_ERROR);
}
}
/**
* 웹 페이지의 상태 코드를 확인하는 메서드
*
* @param connection Jsoup Connection 객체
* @throws IOException 연결 오류 발생
*/
public void checkStatusCode(Connection connection) throws IOException {
// 요청 실행
Connection.Response response = connection.execute();
// 상태 코드 확인
int statusCode = response.statusCode();
log.info("statusCode : {}", statusCode);
// 200: 정상, 304: 재접근 -> 캐시호출
if (statusCode == 200 && statusCode == 304) {
log.info("=== 상태코드 점검 성공 ===");
} else {
log.error("=== 상태코드 점검 실패 ===");
GlobalErrorManager.getInstance().addError(ErrorCode.HTTP_STATUS_ERROR);
}
}
}
Jsch 라이브러리
: SSH 서버에 연결하여 원격 명령어 실행, 파일 전송(SFTP) 등이 가능한 Java 기반 라이브러리
다운로드 및 gradle
implementation 'com.jcraft:jsch:0.1.55'
SSH 예제
public class SSHExample {
public static void main(String[] args) {
String user = "your_username"; // SSH 사용자 이름
String host = "your_host"; // SSH 서버 주소
int port = 22; // SSH 포트 (기본값은 22)
String password = "your_password"; // SSH 비밀번호
String command = "ls -l"; // 실행할 명령어
try {
// JSch 객체 생성
JSch jsch = new JSch();
// 세션 생성
Session session = jsch.getSession(user, host, port);
session.setPassword(password); // 비밀번호 설정
// 호스트 키 확인을 하지 않도록 설정 (보안상 주의 필요)
session.setConfig("StrictHostKeyChecking", "no");
// 세션 연결
session.connect();
System.out.println("SSH 세션에 연결되었습니다.");
// 명령어 실행을 위한 Channel 생성
ChannelExec channelExec = (ChannelExec) session.openChannel("exec");
channelExec.setCommand(command); // 실행할 명령어 설정
// 명령어 실행 결과를 받을 InputStream 설정
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
channelExec.setOutputStream(outputStream);
channelExec.connect(); // 명령어 실행
System.out.println("명령어가 실행되었습니다.");
// 명령어 실행 결과 읽기
InputStream inputStream = channelExec.getInputStream();
byte[] buffer = new byte[1024];
int readCount;
while ((readCount = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, readCount); // 결과를 출력 스트림에 기록
}
// 결과 출력
String result = outputStream.toString("UTF-8");
System.out.println("명령어 결과:\n" + result);
// 채널과 세션 종료
channelExec.disconnect();
session.disconnect();
System.out.println("SSH 세션이 종료되었습니다.");
} catch (Exception e) {
e.printStackTrace(); // 예외 발생 시 스택 트레이스 출력
}
}
}
package com.solomonm.HPChecker.manager;
import com.jcraft.jsch.*;
import com.solomonm.HPChecker.config.ErrorCode;
import com.solomonm.HPChecker.config.ServerConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 지정된 호스트 정보를 가지고 서버에 접속하여 서버 상태와 자식 서버를 점검하는 클래스
*
* @author coffebara
* @version 1.0
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ServerChecker {
private final ServerConfig serverConfig;
/**
* 자식 서버 상태를 점검하여 비정상이면 메일링하는 메서드
*
* @param outputByPs ps 명령어로 실행한 결과로 자식 프로세스
*/
public void checkChildServer(String outputByPs) {
String[] lines = outputByPs.split("\n");
int lineCount = lines.length;
// 결과 출력
if (lineCount != 4) {
log.info("=== 자식 프로세서 상태 점검 성공 ===");
} else {
log.error("=== 자식 프로세서 점검 실패 ===");
GlobalErrorManager.getInstance().addError(ErrorCode.PROCESS_CHILD_ERROR);
}
}
/**
* 서버 상태값을 점검하여 비정상이면 메일링하는 메서드
*
* @param status 서버 상태값
*/
public void checkServerStatus(String status) {
if (!status.equalsIgnoreCase("online")) {
log.info("=== 서버 상태 점검 성공 ===");
} else {
log.error("=== 서버 상태 점검 실패 ===");
GlobalErrorManager.getInstance().addError(ErrorCode.SERVER_STATUS_ERROR);
}
}
/**
* 명령어 실행 결과를 정규식을 이용해 Status 값을 추출하는 메서드
*
* @param output 명령어 실행 결과로 읽어온 결과
* @return String 서버 상태값
*/
public String extractStatus(String output) {
Pattern pattern = Pattern.compile("│\\s*\\d+\\s*│\\s*[^│]+\\s*│\\s*[^│]+\\s*│\\s*[^│]+\\s*│\\s*[^│]+\\s*│\\s*\\d+\\s*│\\s*[^│]+\\s*│\\s*[^│]+\\s*│\\s*([^│]+)\\s*│");
Matcher matcher = pattern.matcher(output.toString());
String status = "";
if (matcher.find()) {
status = matcher.group(1).trim();
log.info("server status: {}", status);
} else {
log.error("Status not found.");
}
return status;
}
/**
* SSH 세션을 통해 원격 서버에서 명령어를 실행하고 그 결과를 반납하는 메서드
*
* @param session 연결된 SSH 객체
* @param command 실행할 명령어
* @return String 명령어 실행 결과
* @throws JSchException SSH 연결 과정에서 발생한 예외
* @throws IOException 명령어 실행 결과를 읽는 과정에서 생기는 입출력 예외
*/
public String executeCommand(Session session, String command) throws JSchException, IOException {
Channel channel = session.openChannel("exec");
((ChannelExec) channel).setCommand(command);
channel.setInputStream(null);
((ChannelExec) channel).setErrStream(System.err);
InputStream in = channel.getInputStream();
channel.connect();
StringBuilder outputBuffer = new StringBuilder();
byte[] tmp = new byte[1024];
while (true) {
while (in.available() > 0) {
int i = in.read(tmp, 0, 1024);
if (i < 0) {
break;
}
outputBuffer.append(new String(tmp, 0, i));
}
if (channel.isClosed()) {
log.info("Exit status: " + channel.getExitStatus());
break;
}
}
channel.disconnect();
return outputBuffer.toString();
}
/**
* SSH 서버에 연결하고 세션을 반환하는 메서드
* JSch 라이브러리를 사용하여 SSH 연결설정
*
* @return 연결된 서버 세션을 반납
* @throws JSchException SSH 연결 실패시 생기는 예외
*/
public Session connectToServer() throws JSchException {
log.info("Connecting to " + serverConfig.getHost());
JSch jsch = new JSch();
Session session = jsch.getSession(serverConfig.getUser(), serverConfig.getHost(), serverConfig.getPort());
session.setPassword(serverConfig.getPassword());
session.setConfig("StrictHostKeyChecking", "no");
session.connect(30000); // 30초 타임아웃
log.info("Connected to " + serverConfig.getHost());
return session;
}
/**
* 예외처리를 위한 메서드
*
* @param e 발생한 예외 객체
*/
public void exceptionHandler(Exception e) {
//공통 처리
System.out.println("=== 개발자용 디버깅 메세지 ===");
e.printStackTrace(System.out); //스택 트레이스 출력
}
}
Spring Boot Mail
: SMTP(Simple Mail Transfer Protocol)를 통해 이메일 메시지를 제공하는 데 필요한 기능 설정과 구현을 제공
Gradle
implementation 'org.springframework.boot:spring-boot-starter-mail:3.3.5'
application.yml 설정
spring:
mail:
host: smtp.example.com # SMTP 서버 주소
port: 587 # SMTP 포트 (TLS의 경우 일반적으로 587 또는 465)
username: your_email@example.com # SMTP 서버 로그인에 사용할 이메일 주소
password: your_password # 해당 이메일 계정의 비밀번호
properties:
mail:
smtp:
auth: true # SMTP 인증 필요 여부
starttls:
enable: true # STARTTLS 사용 여부
ssl:
enable: true # SSL 사용 여부
예제
@Service // 이 클래스가 서비스 컴포넌트임을 나타냄
public class EmailService {
@Autowired // JavaMailSender를 자동으로 주입받음
private JavaMailSender mailSender;
// 이메일 전송 메서드
public void sendSimpleEmail(String to, String subject, String body) {
SimpleMailMessage message = new SimpleMailMessage(); // SimpleMailMessage 객체 생성
message.setTo(to); // 수신자 설정
message.setSubject(subject); // 제목 설정
message.setText(body); // 본문 설정
mailSender.send(message); // 메일 전송
}
}
적용
/**
* 지정된 이메일로 발생한 에러코드와 메세지를 담아 전송하는 클래스
*
* @author coffebara
* @version 1.0
*/
@Component
@RequiredArgsConstructor
public class EmailManager {
private final JavaMailSender mailSender;
@Value("${spring.mail.properties.mail.to}")
private String to;
@Value("${spring.mail.properties.mail.from}")
private String from;
private final String SUBJECT = "[] 홈페이지 오류 보고서 ";
/**
* 에러 코드들을 html로 담아 지정된 이메일로 메일을 전송하는 메서드
*
* @param errorCodeList 발생한 에러코드 리스트
* @throws MessagingException 메일 송신중 발생한 에러
*/
public void sendSimpleMessage(List<ErrorCode> errorCodeList) throws MessagingException {
// 현재 시간
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
String formattedNow = now.format(formatter);
// html 형식 에러 코드
String errorHtml = getText(errorCodeList);
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, false);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(SUBJECT + formattedNow);
helper.setText(errorHtml, true);
mailSender.send(message);
}
/**
* 에러코드들을 html 형식으로 만들어 반환하는 메서드
*
* @param errorCodeList 에러 코드들을 담은 리스트
* @return String 에러코드를 담은 html 형식
*/
private String getText(List<ErrorCode> errorCodeList) {
StringBuilder htmlContent = new StringBuilder();
htmlContent.append("<h1>[ 홈페이지] 오류 보고서</h1>");
// 오류 항목 추가
for (ErrorCode error : errorCodeList) {
htmlContent.append("<li><strong>코드:</strong> ").append(error.getCode())
.append(", <strong>메시지:</strong> ").append(error.getMessage()).append("</li>");
}
htmlContent.append("</ul>")
.append("<p>감사합니다.</p>")
.append("</body>")
.append("</html>");
return htmlContent.toString();
}
}
'Backend > 프로젝트' 카테고리의 다른 글
갈아만든 임원 - 8 엑셀 데이터 입력 (0) | 2024.10.06 |
---|---|
갈아만든 임원 - 7 데이터 추출하기3 클립보드 접근 (1) | 2024.10.05 |
갈아만든 임원 - 6 데이터 추출하기2 OpenCV 전처리 작업 (2) | 2024.09.25 |
갈아만든 임원 - 5 데이터 추출하기2 OpenCV 설치 (1) | 2024.09.25 |
갈아만든 임원 - 4 데이터 추출하기 (Tesseract OCR) (0) | 2024.09.23 |