Notice
Recent Posts
Recent Comments
Link
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Archives
Today
Total
관리 메뉴

JustDoEat

django에서 docker + ELK Stack을 사용해서 직접 로그 생성과, 연동 대쉬보드를 만들어보자 본문

카테고리 없음

django에서 docker + ELK Stack을 사용해서 직접 로그 생성과, 연동 대쉬보드를 만들어보자

kingmusung 2024. 2. 7. 22:33

 

filebeat와 ELK스택을 이용해 Nginx의 로그를 가볍게 연동만 하는 작업을 했었다.

그와 더불어 Django의 로그또한 연동을 해보고 싶었는데, 도커의 볼륨 컨테이너 등에 대한 이해도가 현저히 떨어졌기때문에 연동하기가 더 쉽지 않았다.

또한 페이지가 너무 복잡해 키바나로 대쉬보드 조차 만들어보지 못했다. 진짜 딱 연동만 해봤었다.

다른 테스크 하면서 시간이 날때마다 틈틈히 해서 드디어 연동과 대쉬보드 만드는 법을 알아냈다.

추가로 도커에대한 이해도 또한 같이 올라간거 같아서 기분이 좋다.

 

https://kingmusung.tistory.com/25

 

백엔드 로그를 찍어보자

api에 간단한 로그를 찍어보자! elk를 연동하던중.. 프론트엔드 쪽 로그가 아닌 백엔드쪽 로그를 만들어서 elasticsearch에 보내보고 싶어서 시작을 하였지만, 아래 방식처럼 찍으면 elasticsearch에서 인

kingmusung.tistory.com

 

Nginx의 로그와 Django의 access.log, error.log 만으로는 내가 원하는 결과가 출력이 안되어서 직접 만들어서 연동을 해보겠다.

 

위 링크는 전에 가볍게 로그를 찍는 법만 적어놓았다.

 

https://kingmusung.tistory.com/23

 

위 글은 우여곡절 끝에 기초세팅만 해놓은 상태이고, Nginx 로그만 연동 및 기본적인 세팅만 되어있습니다. 

🔥기본세팅

settings.py

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,  # 디폴트 : True, 장고의 디폴트 로그 설정을 대체. / False : 장고의 디폴트 로그 설정의 전부 또는 일부를 다시 정의
    'formatters': {  # message 출력 포맷 형식
        'verbose': {
            'format': "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s",
            'datefmt': "%d/%b/%Y %H:%M:%S"
        },
        'simple': {
            'format': '%(levelname)s %(message)s'
        },
    },
    'handlers': {
        'member_file': {
            'level': 'INFO',
            'class': "logging.FileHandler",
            'filename': os.path.join(BASE_DIR, 'logs') + "/backend.log"
        },
        'calendar_file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': os.path.join(BASE_DIR, 'logs') + "/backend.log"

        },
        'diary_file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': os.path.join(BASE_DIR, 'logs') + "/backend.log"

        },
        'static_file': {
            'level': 'INFO',
            'class': 'logging.FileHandler',
            'filename': os.path.join(BASE_DIR, 'logs') + "/backend.log"

        },

    },
    'loggers': {
        'member': {
            'handlers': ['member_file'],
            'propagate': True,
            'level': 'INFO',
        },
        'harucalendar': {
            'handlers': ['calendar_file'],
            'propagate': True,
            'level': 'INFO',
        },
        'diary': {
            'handlers': ['diary_file'],
            'propagate': True,
            'level': 'INFO',
        },
        'static': {
            'handlers': ['static_file'],
            'propagate': True,
            'level': 'INFO',
        },

    }
}

 

요약하자면, handler에 어떤 형식의 핸들러를 사용할지 정의를 해주고, 로그를 저장 할 디렉터리와 로그레벨을 설정해주었다. 사실 지금 보면 같은 디렉터리에 같은 파일에 로그를 저장할 의도였으면 하나의 핸들러만 정의 했어도 될거같다.

(초기에는 각각 로그파일로 관리하기위한 의도였음.)

 

그 아래 logger에는 어떤 handler를 사용할지 정의해주고 로그레벨을 기록해주면 된다.

 

Views.py 의 코드 일부분

logger.info(f'INFO {client_ip} {current_time} GET /calendars 200 calendar is inquired')
logger.info(f'INFO {client_ip} {current_time} POST /calendars 200 calendar Stickers are saved')
logger.warning(f'WARNING {client_ip} {current_time} POST api/v1/diaries//DiriesPost 400 {calendar_serializer.errors}')

 

로그를 찍을때도 일관적인 형식을 가지고 찍어야한다는걸 깨달았다. 나의 경우에는

로그레벨, 아이피, 시간, 요청방식, url, 상태코드, 메시지 순서로 로그를 찍었다.

 

backend.log

INFO 127.0.0.1 2024-02-04 05:09:01 POST /members 200 login success 
INFO 127.0.0.1 2024-02-04 05:23:20 POST /members 200 login success 
INFO 127.0.0.1 2024-02-04 05:23:39 POST /members 200 login success 
INFO 127.0.0.1 2024-02-04 05:23:39 POST /members 200 login success 

 

api요청을 하면 이런식으로 로그파일에 로그가 찍히게 된다.

 


 

🔥 Logstash setting.

 

pipline.yml

input {
  file {
    path => "/logs/backend.log"
    start_position => "beginning"
  }
}

filter {
  grok {
    match => { "message" => "%{LOGLEVEL:loglevel} %{IP:ip_address}
    %{TIMESTAMP_ISO8601:timestamp} %{WORD:http_method} %{URIPATH:http_path} %{NUMBER:http_status} %{GREEDYDATA:log_message}" }
  }

   date {
    match => [ "timestamp", "ISO8601" ]
  }
}

output {
  elasticsearch {
    hosts => ["http://elasticsearch:9200"]
    index => "member-%{+YYYY.MM.dd}"
    user => "elastic"
    password => "changeme"
  }
  stdout { codec => rubydebug }
}

 

input {
  file {
    path => "/logs/backend.log"
    start_position => "beginning"
  }
}

 

path 이 부분은 logstash 컨테이너 내부에 /logs/backend.log 경로에 있는 backend.log파일을 입력값으로 넣겠다는말이다.

 

내 로컬에 있는 로그파일이 컨테이너 내부로 그냥 마운트 되지는 않을것이다.

docker-compose.yml(logstash 부분, 불필요한 코드는 생략)

logstash:
#전부 생략
  volumes:
    - ./haruProject/logs/:/logs/
 #전부 생략

 

로그스테쉬의 볼륨을 설정을 해줌으로 로컬에 있던 로그파일이 logstash의 컨테이너 내부 경로로 마운트되게 해준다.

 

./haruProject/logs/ 에 있는 모든 파일을, 컨테이너 내부 logs라는 디렉터리 하위에 위치하겠다는 말.

./ 이부분은 docker-compose.yml 이 위치한 현제 경로를 말 하는거임.

즉 haruProject는 docker-compose.yml이랑 같은 위치에 있다는말

 


filter {
  grok {
    match => { "message" => "%{LOGLEVEL:loglevel} %{IP:ip_address} %{TIMESTAMP_ISO8601:timestamp} %{WORD:http_method} %{URIPATH:http_path} %{NUMBER:http_status} %{GREEDYDATA:log_message}" }
  }

   date {
    match => [ "timestamp", "ISO8601" ]
  }
}

 

필터부분인데 이 부분이 너무 어려웠었다.

 

일단 Grok부터 살펴보면

Grok이란?

사용자가 정의한 패턴을 사용하여 텍스트를 추출, 구조화를 하는 역할을한다.

logger.info(f'INFO {client_ip} {current_time} GET /calendars 200 calendar is inquired')
"%{LOGLEVEL:loglevel} %{IP:ip_address} %{TIMESTAMP_ISO8601:timestamp} %{WORD:http_method} %{URIPATH:http_path} %{NUMBER:http_status} %{GREEDYDATA:log_message}" }

 

내가 정의한 로그 형식과, grok필터에서 정의한 부분을 비교해보면 한가지 패턴을 찾을 수 있다.

 

loglevel -> INFO, ip_address -> {client_ip}, timestamp -> {current_time}, http_method -> GET, http_path -> /calendar, 

 

http_status -> 200, log_message-> calendar is required

이런식으로 매치가 될것이다.

 

LOGLEVEL, IP TIMESTAMP_IS08601 처럼 대문자로 되어진 부분은 엘라스틱 서치가 인식할 수 있는 정해진 변수명이므로 임의로 바꾸면 안된다.

 

loglevel, ip_address, timestamp등 소문자로 된 부분은 사용자가 정의 할 수 있는 변수명이다 이 변수명은 나중에 시각화를 하는 과정에서 우리가 원하는 정보를 볼 수 있게 해주는 변수명이다. 

 

 

(위 사진은 지금까지 작업을 거친 후 키바나로 대쉬보드를 생성 했을때 볼 수 있는 화면중 일부이다. 이해를 위해 첨부했다.)

 

 

 


🏝️ Elastic Search

이제 기본적인 설정을 하였으니, 로그가 잘 넘어왔는지 확인을 해보자

 

위 작업을 거친 후 토커컴포즈를 이용하여 컨테이너를 실행시켜준다

 

 

컨테이너에서 ElasticSearch의 포트번호로 (http://localhost:9200/) 접속 후 위와 같은 화면이 뜨면 잘 실행이 되고있는것이다.

 


 

그 후 키바나의 포트번호로(http://localhost:5601/app/home#/) 들어가게 되면 위와같은 화면이 나오게 되고.

 

좌측 맨 하단에 Stack Management -> Index Management 까지 들어가주면

 

 

 

위 사진처럼 로그들이 넘어오는걸 확인 할 수 있다.

만약 아무것도 없다면,

 

✅ Docker 볼륨부분을 잘 확인해 보는걸 추천한다, 필자의 경우는 마운트를 잘못시켜서 로그가 잘 안넘어왔었다.

 

✅ Logstash의 필터부분에 표현식이 맞는지 검토를 해보는걸 추천한다.

 


🏖️ Kibana DashBoard

 

인덱스가 넘어오는걸 확인을 하였다면.  아까처럼 좌측 하단이 아닌 좌측 상단에 보면 Dashboard라는 항목이 보이는데 클릭을 해준다.

 

그 후 create data view 를 클릭해주고.

 

 

index pattern에 본인이 대쉬보드를 만들기 희망하는 인덱스 이름을 적어주고,(우측에 뜨는 이름을 적어야함, 임의로 적으면X)

 

그후 이름도 적절하게 지어준후 save data view to kibana를 눌러준다

 

그후 뜨는 창에서 create a dashboard -> dashboard visualization 을 눌러준다

 

 

그 후 우측에 보면 내가 설정한 로깅 포멧대로 나열이 되어있는데. 본인이 필요한 정보들을 사용하여 대쉬보드를 만들면 된다.

 

이 이후는 직관적으로 할 수 있어서 설명을 따로 적어놓지는 않겠습니다.

 

저같은 경우는 예시로 상태코드들만 볼 수 있도록 해놨습니다.

(글 처음에 위 처럼 설정한 로그 포멧중 상태코드에 해당하는 부분들로 대쉬보드를 만든거임.)

logger.info(f'INFO {client_ip} {current_time} GET /calendars 200 calendar is inquired')
logger.warning(f'WARNING {client_ip} {current_time} POST api/v1/diaries//DiriesPost 400 {calendar_serializer.errors}')

 

 


좌측에 정보가 부실하다던가, 내가 설정한 정보가 안나온다 하는 문제는 Logstash의 Filter 부분과 내가 정의한 로그 포멧과 일치하지 않아서 그럴 수 있습니다.(네 제가 그랬습니다)