Spring
Spring + ELK로 로그 시스템 구축하기
minturtle
2024. 11. 16. 00:05
반응형
개요
- Ticketing 프로젝트 진행 중에 로그를 시각화하기 위해서 ELK 스택을 도입하기로 결정하였습니다. 원래는 모니터링 서버(Grafana)에 Loki와 연동하였으나, 모니터링 서버의 성능이 상대적으로 떨어져 정상적으로 로그를 처리하지 못했고, ElasticSearch도 사용하게 된 김에 이를 해결하기 위해 ELK를 도입하기로 결정했습니다.
1. Spring 로그 설정
- 먼저 ELK로 로그를 보내기 위해선, 스프링에서 찍은 로그를 파일로 저장해야합니다. 로그는 LogstashEncoder를 사용하여 JSON 형태로 로그가 저장되도록 설정하였습니다.
<configuration>
<timestamp key="TODAY" datePattern="yyyyMMdd"/>
<appender name="LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${LOG_FILE}_${TODAY}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_FILE}.%d{yyyyMMdd}_%i.log</fileNamePattern>
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE}</maxFileSize>
<totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP}</totalSizeCap>
<maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY}</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<root level="INFO">
<appender-ref ref="LOG_FILE"/>
</root>
</configuration>
{"@timestamp":"2024-11-08T21:14:04.3193516+09:00","@version":"1","message":"[UcpDYLQ4]|||-> PerformanceReader.findPerformanceEntityByCursor","logger_name":"com.flab.ticketing.common.aop.aspect.LoggingAspect","thread_name":"http-nio-8080-exec-12","level":"INFO","level_value":20000,"traceId":"672e008cbfe531720077d34b580681a0","spanId":"c44823ae293841f0"}
- 참고로 LOG_PATH, LOG_FILE 등은 설정을 보고 쉽게 yml 파일로 등록할 수도 있습니다.
logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%level] [%thread] [%logger{36}] - %msg%n"
file:
name: ticketing
path: /app/logs
logback:
rollingpolicy:
max-file-size: 10MB
total-size-cap: 1GB
max-history: 30
- 이렇게 하면 /app/logs 디렉터리에 로그파일이 저장됩니다.
2. Logstash와 Kibana 구축하기
- 그 후 ElasticSearch, Logstash와 Kibana를 띄울 건데, 편하게 Docker Compose를 사용해서 띄워 보도록 하였습니다.
version: "3.8"
volumes:
certs:
driver: local
esdata01:
driver: local
kibanadata:
driver: local
logstashdata01:
driver: local
networks:
default:
name: elastic
external: false
services:
setup:
image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
volumes:
- certs:/usr/share/elasticsearch/config/certs
user: "0"
command: >
bash -c '
if [ x${ELASTIC_PASSWORD} == x ]; then
echo "Set the ELASTIC_PASSWORD environment variable in the .env file";
exit 1;
elif [ x${KIBANA_PASSWORD} == x ]; then
echo "Set the KIBANA_PASSWORD environment variable in the .env file";
exit 1;
fi;
if [ ! -f config/certs/ca.zip ]; then
echo "Creating CA";
bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip;
unzip config/certs/ca.zip -d config/certs;
fi;
if [ ! -f config/certs/certs.zip ]; then
echo "Creating certs";
echo -ne \
"instances:\n"\
" - name: es01\n"\
" dns:\n"\
" - es01\n"\
" - localhost\n"\
" - ${MY_DOMAIN_NAME}\n"\
" ip:\n"\
" - 127.0.0.1\n"\
" - name: kibana\n"\
" dns:\n"\
" - kibana\n"\
" - localhost\n"\
" ip:\n"\
" - 127.0.0.1\n"\
> config/certs/instances.yml;
bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key;
unzip config/certs/certs.zip -d config/certs;
fi;
echo "Setting file permissions"
chown -R root:root config/certs;
find . -type d -exec chmod 750 \{\} \;;
find . -type f -exec chmod 640 \{\} \;;
echo "Waiting for Elasticsearch availability";
until curl -s --cacert config/certs/ca/ca.crt https://es01:9200 | grep -q "missing authentication credentials"; do sleep 30; done;
echo "Setting kibana_system password";
until curl -s -X POST --cacert config/certs/ca/ca.crt -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" https://es01:9200/_security/user/kibana_system/_password -d "{\"password\":\"${KIBANA_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done;
echo "All done!";
'
healthcheck:
test: [ "CMD-SHELL", "[ -f config/certs/es01/es01.crt ]" ]
interval: 1s
timeout: 5s
retries: 120
es01:
depends_on:
setup:
condition: service_healthy
image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
labels:
co.elastic.logs/module: elasticsearch
volumes:
- certs:/usr/share/elasticsearch/config/certs
- ./es-data:/usr/share/elasticsearch/data
ports:
- ${ES_PORT}:9200
environment:
- node.name=es01
- cluster.name=${CLUSTER_NAME}
- discovery.type=single-node
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
- bootstrap.memory_lock=true
- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=true
- xpack.security.http.ssl.key=certs/es01/es01.key
- xpack.security.http.ssl.certificate=certs/es01/es01.crt
- xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt
- xpack.security.transport.ssl.enabled=true
- xpack.security.transport.ssl.key=certs/es01/es01.key
- xpack.security.transport.ssl.certificate=certs/es01/es01.crt
- xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt
- xpack.security.transport.ssl.verification_mode=certificate
- xpack.license.self_generated.type=${LICENSE}
mem_limit: ${ES_MEM_LIMIT}
ulimits:
memlock:
soft: -1
hard: -1
healthcheck:
test: [ "CMD-SHELL", "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'" ]
interval: 10s
timeout: 10s
retries: 120
kibana:
depends_on:
es01:
condition: service_healthy
image: docker.elastic.co/kibana/kibana:${STACK_VERSION}
labels:
co.elastic.logs/module: kibana
volumes:
- certs:/usr/share/kibana/config/certs
- kibanadata:/usr/share/kibana/data
ports:
- ${KIBANA_PORT}:5601
environment:
- SERVERNAME=kibana
- ELASTICSEARCH_HOSTS=https://es01:9200
- ELASTICSEARCH_USERNAME=kibana_system
- ELASTICSEARCH_PASSWORD=${KIBANA_PASSWORD}
- ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES=config/certs/ca/ca.crt
- XPACK_SECURITY_ENCRYPTIONKEY=${ENCRYPTION_KEY}
- XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY=${ENCRYPTION_KEY}
- XPACK_REPORTING_ENCRYPTIONKEY=${ENCRYPTION_KEY}
mem_limit: ${KB_MEM_LIMIT}
healthcheck:
test: [ "CMD-SHELL", "curl -s -I http://localhost:5601 | grep -q 'HTTP/1.1 302 Found'" ]
interval: 10s
timeout: 10s
retries: 120
logstash01:
depends_on:
es01:
condition: service_healthy
kibana:
condition: service_healthy
image: docker.elastic.co/logstash/logstash:${STACK_VERSION}
labels:
co.elastic.logs/module: logstash
user: root
volumes:
- certs:/usr/share/logstash/certs
- logstashdata01:/usr/share/logstash/data
- "./logstash_ingest_data/:/usr/share/logstash/ingest_data/"
- "./logstash.conf:/usr/share/logstash/pipeline/logstash.conf:ro"
ports:
- "5044:5044"
environment:
- xpack.monitoring.enabled=false
- ELASTIC_USER=elastic
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
- ELASTIC_HOSTS=https://es01:9200
ELK Docker Compose 작성은 공식 문서와 깃허브를 보고 제가 필요한 부분만 변경하였습니다.
각 컨테이너의 역할은 다음과 같습니다
- setup : elasticsearch-certutil를 사용하여 CA 인증서를 발급하고, Kibana가 ElasticSearch에 접속할 수 있는 User를 생성하는 역할을 합니다.
- CA인증서는 자체 서명을 하기 때문에 JAVA 프로그램을 실행할 때 “신뢰되지 않는 인증서” 오류가 나타날 수 있습니다. 이에 대해 저는 인증서만 있으면 무조건 신뢰하도록 코드를 작성하였습니다.
@Configuration
@Profile("prod")
internal class ProductionElasticSearchConfig(
@Value("\${spring.data.elasticsearch.url}") private val elasticHost: String,
@Value("\${spring.data.elasticsearch.api-key}") private val elasticApiKey: String
) : ElasticsearchConfiguration() {
override fun clientConfiguration(): ClientConfiguration {
val httpHeaders = HttpHeaders()
httpHeaders.add("Authorization", "ApiKey $elasticApiKey")
// SSL Context 설정
val sslContext = SSLContextBuilder.create()
.loadTrustMaterial(null) { _: Array<X509Certificate>, _: String -> true }
.build()
return ClientConfiguration.builder()
.connectedTo(elasticHost)
.usingSsl(sslContext)
.withDefaultHeaders(httpHeaders)
.build()
}
}
- 다음으론 Kibana사용자 생성인데요, 기본 사용자(elastic)으로 접근하도록 하면되지 않냐? 라고 할 수 있는데 ElasticSearch 8 버젼 이상에서 Kibana가 기본 사용자를 사용할 수 없도록 제한되어 있음을 확인할 수 있었습니다.
- es01
- elasticsearch 컨테이너입니다. es-data 폴더를 볼륨으로 걸어 컨테이너를 재실행해도 데이터가 유실되지 않도록 설정되어 있습니다.
- kibana
- kibana 컨테이너입니다.
- logstash01
- logstash 컨테이너 입니다. logstash는 logstash.conf를 필요로 하는데, 이는 아래와 같습니다.
input {
beats {
port => 5044
host => "0.0.0.0"
}
}
filter {
json {
source => "message"
skip_on_invalid_json => true
}
}
output {
elasticsearch {
index => "logstash-%{+YYYY.MM.dd}"
hosts=> "${ELASTIC_HOSTS}"
user=> "${ELASTIC_USER}"
password=> "${ELASTIC_PASSWORD}"
cacert=> "certs/ca/ca.crt"
}
}
- logstash는 beat가 5044 포트로 데이터를 전송하면, 이를 elasticsearch에 저장합니다.
- 중간의 filter는 데이터가 JSON 형태로 들어오는 것을 파싱하여 더 보기 편하게 파싱하는 역할을 하는데요, 설명하기가 어려워 예시를 보여드리겠습니다.
/// BEFORE
{
"something" : "",
// ...
"message" : {"level" : "info", "message" : "Tomcat started on port 9090 (http) with context path '/'"}
}
// After
{
"something": "",
"level" :"info",
"message" : "Tomcat started on port 9090 (http) with context path '/'",
}
3. FileBeat 설정하기
- 그 후 Application 서버로 가서, FileBeat를 설정해 주었습니다.
logging.level: info
logging.to_stderr: true
filebeat.inputs:
- type: filestream
id: logback-logs
enabled: true
paths:
- ${LOG_PATH}/*.log
json.keys_under_root: true
json.add_error_key: true
json.message_key: message
processors:
- add_docker_metadata: ~
output.logstash:
hosts: ["logstash:5044"]
ssl.enabled: false
- filebeat는 log 파일을 읽어서, logstash로 보내는 설정입니다.
- 마지막으로 docker compose로 filebeat를 실행할 수 있도록 설정해 주었습니다.
filebeat:
image: docker.elastic.co/beats/filebeat:8.13.4
container_name: filebeat
user: root
volumes:
- ./logs:/app/logs
- ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
environment:
- LOG_PATH=/app/logs
command:
- --strict.perms=false
- 여기서 user를 root로 설정하지 않으면 아래의 오류가 나는 것을 확인할 수 있었습니다.
Exiting: error loading config file: config file ("filebeat.yml") must be owned by the user identifier (uid=0) or root
Kibana 접속 및 설정
- 마지막으로 Kibana에 접속해서 설정 해주면 되었는데요, 먼저 Data View를 설정해주어야 합니다. 공식 문서는 여기서 볼 수 있습니다.
- 저는 logstash.conf에서도 확인할 수 있듯이 logstash-yyyy.MM.dd 와 같이 인덱스를 생성하였기 때문에, index pattern에 logstash-* 로 설정하면 모든 로그 인덱스가 매칭됨을 알 수 있었습니다.
- 그러면 Kibana Discover에서 로그를 검색하고 확인할 수 있습니다. 공식문서는 여기에서 확인할 수 있습니다.
반응형