
MSA와 같이 여러 서버를 운영할 때 각각의 log들을 확인하기 위해서는 각 서버에 들어가 로그를 봐야하는 과정들이 있다.
나는 그래서 이러한 과정을 좀 더 간편하게 할 수 없을까 고민하던 중 log & crash를 알게 되었다.
클라이언트와 서버의 로그를 수집하여 사용자가 원하는 로그를 검색하고 조회할 수 있으며, 모바일 앱에서 발생하는 크래시 리포트를 수집하고 분석하여 크래시 발생 원인에 대한 다양한 정보를 제공한다.
간단히 설명하자면 분산되어 있는 log를 한곳에 취합하여 볼 수 있고 로그를 검색 및 조회 분석할 수 있다.
nhn cloud log & crash는 API 형식으로 log를 주고 받는다.
분산되어 있는 log를 정해진 형태에 맞춰 보낸다.
JSON과 HTTP로 Log & Crash 수집 서버에 로그를 전송할 때는 다음 주소를 사용해다 한다.
- Log & Crash: api-logncrash.nhncloudservice.com
- Method of Delivery: POST
- URI: /v2/log
- Content-Type: "application/json"
{
"projectName": "__앱키__",
"projectVersion": "1.0.0",
"logVersion": "v2",
"body": "This log message come from HTTP client.",
"logSource": "http",
"logType": "nelo2-log",
"host": "localhost"
}
Log Search를 위한 파라미터
projectName: string, 필수
[in] 앱키.
projectVersion: string, 필수
[in] 버전. 사용자 지정 가능. "A~Z, a~z, 0~9,-._"만 포함.
body: string, 옵션
[in] 로그 메시지.
logVersion: string, 필수
[in] 로그 포맷 버전. "v2".
logSource: string, 옵션
[in] 로그 소스. Log Search에서 필터링을 위해 사용. 정의되지 않으면 "http".
logType: string, 옵션
[in] 로그 타입. Log Search에서 필터링을 위해 사용. 정의되지 않으면 "log".
host: string, 옵션
[in] 로그를 보내는 단말의 주소. 정의되지 않으면 수집 서버에서 peer-address를 사용해 자동으로 채움.
sendTime; string, 옵션
[in] 단말이 보낸 시간. 입력 시 Unix timestamp로 입력.
logLevel; string, 옵션
[in] Syslog 이벤트용.
UserBinaryData; string, 옵션
[in] 로그 검색 화면에서 [다운로드|보기] 링크 표시, base64 인코딩된 값을 담아 전송.
UserTxtData; string, 옵션
[in] 로그 검색 화면에서 [다운로드|보기] 링크 표시, base64 인코딩된 값을 담아 전송.
txt*; string, 옵션
[in] 필드 이름이 txt로 시작하는 필드(txtMessage, txt_description 등)는 text 필드로 저장. 로그 검색 화면에서 필드값의 일부 문자열로 검색(full text search) 가능. 필드의 크기는 1MB로 제한됨.
long*; long, 옵션
[in] 필드 이름이 long으로 시작하는 필드(longElapsedTime, long_elapsed_time 등)는 long 타입 필드로 저장됨. 로그 검색 화면에서 long 타입 range 검색 가능.
double*; double, 옵션
[in] 필드 이름이 double로 시작하는 필드(doubleAvgScore, double_avg_score 등)는 double 타입 필드로 저장됨. 로그 검색 화면에서 double 타입 range 검색 가능.
저는 Spring의 logback의 Appender를 통하여 RestTemple을 사용해 API를 전송하였습니다.


@Setter
@Getter
public class LogCrashRequest {
String projectName;
String projectVersion;
String logVersion;
String body;
String sendTime;
String logSource;
String logType;
String host;
private static final String DEFAULT_PROJECT_NAME = "APIKey 입력";
private static final String DEFAULT_PROJECT_VERSION = "1.0.0";
private static final String DEFAULT_LOG_VERSION = "v2";
private static final String DEFAULT_LOG_SOURCE = "http";
private static final String DEFAULT_LOG_TYPE = "nelo2-http";
private static final String DEFAULT_HOST = "localhost";
public LogCrashRequest(String body) {
this.body = body;
this.projectName = DEFAULT_PROJECT_NAME;
this.projectVersion = DEFAULT_PROJECT_VERSION;
this.logVersion = DEFAULT_LOG_VERSION;
this.logSource = DEFAULT_LOG_SOURCE;
this.logType = DEFAULT_LOG_TYPE;
this.host = DEFAULT_HOST;
}
}
각 항목은 각자의 프로젝트에 맞게 설정해주시면 됩니다.
@Setter
@Slf4j
public class LogCrashAppender extends AppenderBase<ILoggingEvent> {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final RestTemplate restTemplate = new RestTemplate();
private String url;
@Override
protected void append(ILoggingEvent iLoggingEvent) {
LogCrashRequest request = new LogCrashRequest(iLoggingEvent.getFormattedMessage());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
try {
String str = objectMapper.writeValueAsString(request);
HttpEntity<String> body = new HttpEntity<>(str, headers);
restTemplate.postForEntity(url, body, String.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
log 내용을 body로 넣어 restTemplate을 통해 보낸다.
<appender name="NHN_LOG_CRASH_APPENDER" class= "logcarshAppender의 패키지 위치.LogCrashAppender">
<url>https://api-logncrash.cloud.toast.com/v2/log</url>
</appender>
<appender name="ASYNC_NHN_LOG_CRASH_APPENDER" class="ch.qos.logback.classic.AsyncAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %magenta([%thread]) %highlight([%-3level]) %logger{5} - %msg %n</pattern>
</encoder>
<appender-ref ref="NHN_LOG_CRASH_APPENDER"/>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC_NHN_LOG_CRASH_APPENDER" />
</root>
logcarshAppender의 패키지 위치.LogCrashAppender 이 부분은 각자에 맞게 패키치 위치를 맞추시면 됩니다,
level은 INFO 로 설정했습니다.
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<springProperty scope="context" name="LOG_DIR" source="log.directory" />
<timestamp key="BY_DATE" datePattern="yyyy-MM-dd" />
<springProfile name="dev">
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</springProfile>
<springProfile name="prod">
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<appender name="FILE-ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/error/error-${BY_DATE}.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR}/backup/error/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="NHN_LOG_CRASH_APPENDER" class="com.nhnacademy.bookstore.global.appender.LogCrashAppender">
<url>https://api-logncrash.cloud.toast.com/v2/log</url>
</appender>
<appender name="ASYNC_NHN_LOG_CRASH_APPENDER" class="ch.qos.logback.classic.AsyncAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %magenta([%thread]) %highlight([%-3level]) %logger{5} - %msg %n</pattern>
</encoder>
<appender-ref ref="NHN_LOG_CRASH_APPENDER"/>
</appender>
<appender name="FILE-INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/info/info-${BY_DATE}.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR}/backup/info/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="FILE-ERROR" />
<appender-ref ref="FILE-INFO" />
<appender-ref ref="ASYNC_NHN_LOG_CRASH_APPENDER" />
<appender-ref ref="CONSOLE" />
</root>
</springProfile>
</configuration>
제가 이전 프로젝트를 할때 사용한 logback-spring.xml 파일입니다.
dev에서는 콘솔에서만 로그가 보이도록 하고
prod에서는 콘솔, 파일로 저장(info, error), log & crash 사용
- log crash는 API를 통해 분리되어있는 환경의 로그들을 모아서 검색 및 조회를 할 수 있다.
- Appedner 기능을 통해서 로그에 관련된 내용을 처리할 수 있다.
- RestTemple으로 API 요청을 주고 받을 수 있다.