Django(장고) 로깅 기초

장고 로깅

현재 운영하고 있는 식단알리미의 사용자들이 어떠한 버튼을 누르는지 분석하기 위해 방법을 찾던 도중 사용자 입력에 대한 로그를 남기는 게 좋을 것 같아서 장고의 로깅하는 법에 대해서 간략하게 정리해보려고한다.

일단 장고의 로깅 시스템은 기본적으로 파이썬의 로깅 시스템을 그대로 따르면서 일부만 추가되었다고 한다. 파이썬의 로깅 모듈을 보면 logger, handler, filter, formatter 4가지의 컴포넌트를 정의하고 있다. 네 가지 로깅 컴포넌트들에 대해서 정리하자.

컴포넌트

logger

logger는 로깅 시스템의 시작점이다. 로그 메세지를 처리하기 위해 메세지를 담아두는 저장소라고 생각할 수 있으며, 모든 로거는 이름을 가지고 있다. 또한 logger는 logger level을 갖고 있으며, 이는 로그 메세지의 중요도에 따라 자신이 어느 레벨 이상의 메세지를 처리할지에 대한 기준이 된다. 아래는 다섯 가지 log level 이다.

  • DEBUG : 디버그 용도로 사용되는 정보. 로그 레벨의 최하위 수준이다
  • INFO : 일반적이고 보편적인 정보(Django의 기본 로그 레벨값이다)
  • WARNING : 문제점 중에서 덜 중요한 문제점이 발생 시 이에 대한 정보
  • ERROR : 문제점 중에서 주요 문제점이 발생 시 이에 대한 정보
  • CRITICAL : 치명적인 문제점 발생 시 이에 대한 정보. 로그 레벨의 최상위 수준이다

로거에 저장되는 메세지를 로그 레코드(log record)라고 부른다. 마찬가지로 로그 레코드도 로그 레벨을 가진다.

메세지가 로거에 도착하면, 로그 레코드의 로그 레벨과 로거의 로그 레벨을 비교한다. 로그 레코드의 로그 레벨이 로거의 로거 레벨과 같거나 높으면 로거는 메세지 처리를 계속하게 된다. 만약 그렇지 않다면 해당 로그 메세지는 무시된다. 이와 같은 과정을 거쳐 로거는 로그 메세지를 핸들러에게 넘기게 된다.

1
2
3
4
5
6
'loggers': {
'board': {
'handlers': ['console'],
'level': 'DEBUG',
}
},

handler

핸들러는 로거에 있는 로그 메세지에 어떠한 작업을 할지 결정하는 엔진이다. 로거로 부터 받은 메세지를 화면에 스트림으로 출력하거나 파일로 저장하거나 하는 등의 로그 동작을 의미한다. 핸들러도 로거와 마찬가지로 로그 레벨을 가지고 있다. 로그 레코드의 로그 레벨이 핸들러의 로그 레벨보다 더 낮으면 핸들러는 메세지를 무시하게 된다.

1
2
3
4
5
6
7
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'format2',
}
},

filter

로그 레코드가 로거에서 핸들러로 넘어갈 때, 필터를 사용해서 로그 레코드에 부가적인 제어를 할 수 있다. 기본 처리 방식은 로그 레벨을 지정하여 그 로그 레벨에 해당되면 관련 로그 메세지를 처리하는 것인데, 필터를 적용하게 되면 로그 처리 기준을 추가할 수 있다. 예를 들면, 로그 레벨이 ERROR인 로그 레코드 중에서 특정 소스로부터 오는 메세지만 핸들러로 넘길 수 있다.

필터를 사용하면 로그 레코드를 보내기 전에 수정하는 것도 가능하다. 예를 들면, 어떤 조건에 맞으면 ERROR 로그 레코드를 WARNING 로그 레벨로 낮춰주는 필터를 만들 수도 있다. 필터는 로거와 핸들러 모두에게 적용할 수 있고, 여러 개의 필터를 체인 방식으로 동작시킬 수도 있다!

formatter

로그 레코드는 최종적으로 텍스트로 표현된다. 이 때, 포멧터의 역할은 텍스트로 표현 시 임의의 포맷을 지정해주는 것이다. 포맷터는 보통 파이썬의 포맷 스트링을 사용하지만, 사용자 정의 포맷터도 만들어 사용할 수 있다.

1
2
3
4
5
6
7
8
9
'formatters': {
'format1': {
'format': '[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s',
'datefmt': '%d/%b/%Y %H:%M:%S',
},
'format2': {
'format': '[%(levelname)s] %(message)s',
},
},

로깅

로깅을 하기 위한 컴포넌트들이 준비되었다면 로그 메세지를 기록하기 위해 코드 내에서 로깅 메소드를 호출하면 된다.

1
2
3
4
5
6
7
8
9
# logging 모듈 임포트
import logging

# 로거 인스턴스 생성
logger = logging.getLogger(__name__)

def index(request):
logger.error('Something went wrong!') # ERROR 레벨 이상의 로그 레코드 로깅
...

로거 인스턴스는 이름을 가진다. 명시적으로 적어주어도 되지만 name 특수 메소드를 통해서 명명해주는 것이 자주 사용되는 것 같다. 이와 같은 방법으로 로거 이름을 짓는다면 어떻게 출력이 될지 보고 싶을땐

1
2
3
4
# in views.py

print(__name__) # 해당 모듈명 출력 : app.views
print(logger) # 로거 이름 출력 : <Logger app.views>

다른 방식으로 로그 메세지를 계층화하고 싶으면 로거를 구분하는 이름을 도트(.)방식으로 명명해주면 된다.

1
logger = logging.getLogger('project.interesting.stuff')

로거 객체(logger)는 각 로그 레벨별로 로깅 호출 메소드를 갖고 있다.

  • logger.debug()
  • logger.info()
  • logger.warning()
  • logger.error()
  • logger.critical()

각 메소드들은 해당 레벨의 로그 메세지를 생성한다. 또한 두 가지 로깅 메소드를 추가로 제공하고 있다

  • logger.log() : 원하는 로그 레벨을 정해서 로그 메세지를 생성한다
  • logger.exception() : 익셉션 스택 정보를 포함하는 ERROR 레벨의 로그 메세지 생성

로깅 설정

네 가지 컴포넌트들(logger, filter, handler, formatter)을 설정해야 한다. 장고는 dictConfig 설정 방식을 기본으로 사용한다. 프로젝트의 settings.py 파일에 LOGGING 항목을 생성하여 설정을 하면 된다. LOGGING 속성을 정의하는 경우에 디폴트 로그 설정을 대체할 수도 있고, 디폴트 로그 설정과 병합할 수도 있다. 하는 방법은 LOGGING 설정에서 disable_existing_loggers 속성을 통해 지정하면 되는데 값이 True면 장고의 기본 로깅 설정을 대체(덮어쓰기)할 수 있다.

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
32
33
34
35
36
LOGGING = {
'version': 1, # 버전
'disable_existing_loggers': True, # 장고의 기본 로깅 설정 활성화
# 포맷터
'formatters': {
'format1': {
'format': '[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s',
'datefmt': '%d/%b/%Y %H:%M:%S',
},
'format2': {
'format': '[%(levelname)s] %(message)s',
},
},
# 필터
'filters': {
'special': {
'()': 'project.logging.SpecialFilter',
'foo': 'bar',
}
},
# 핸들러
'handlers': {
'console': { # 콘솔에 스트림 출력
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'format1'
}
},
# 로거
'loggers': {
'board': {
'handlers': ['console'],
'level': 'DEBUG'
}
}
}

장고에 추가된 컴포넌트

로거

  1. django 로거
    모든 로그 레코드를 잡는 장고의 최상위 루트 로거이다. 이 루트 로거에 직접 로그 메세지를 보낼 수는 없다.

  2. django.request 로거
    http 요청 처리와 관련된 메세지를 기록한다. 5xx 응답은 ERROR / 4xx 응답은 WARNING 메세지로 발생한다. 이 로거에 담기는 메세지는 2개의 추가적인 메타 항목을 가진다.

    • status_code : http 응답 코드
    • request : 로그 메세지를 생성하는 request 객체
  3. django.db.backends 로거
    데이터베이스와 관련된 메세지를 기록한다. 애플리케이션에서 사용하는 모든 sql 문장들이 이 로거에 debug 레벨로 기록된다. 성능상의 이유로, sql 로깅은 settings.DEBUG 속성이 True일 경우에만 활성화된다.

    • duration : sql 실행에 걸린 시간
    • sql : 실행된 sql 문장
    • params : sql 호출에 사용된 파라미터
  4. django.security.* 로거
    사용자가 보안에 해를 끼칠수 있는 동작을 했을 경우 메세지를 기록한다.

  5. django.db.backends.schema 로거
    데이터베이스의 스키마 변경 시 사용된 sql 쿼리를 기록한다.

핸들러

  1. AdminEmailHandler
    이 핸들러가 수신하는 모든 로그 메세지를 이메일로 사이트 관리자에게 전송

필터

  1. CallBackFilter
    콜백 함수를 지정해서 필터를 통과하는 모든 메세지에 대해 콜백 함수를 호출해준다. 콜백 함수의 리턴값이 False면 메세지 로깅은 더 이상 처리하지 않는다.

  2. RequireDebugFalse
    settings.DEBUG 속성이 False인 경우만 메세지 처리

  3. RequireDebugTrue
    settings.DEBUG 속성이 True인 경우에만 메세지 처리

장고 로깅 default

  • django.request 로거
    settings.DEBUG 속성이 False면 ERROR 또는 CRITICAL 레벨의 모든 메세지를 AdminEmailHandler 핸들러에게 보내주도록 되어있다

  • django 루트 로거
    django 루트 로거에 오는 모든 메세지는 DEBUG 속성이 True인 경우 콘솔로 보내지고, DEBUG 속성이 False인 경우 NullHandler로 보내서 무시되도록 기본값이 정해져있다.

Share

dockerfile의 기본

Dockerfile

Dockerfile은 도커 이미지를 생성할때 실행할 일련의 명령어들을 적어놓은 파일이다. 쉽게 말하자면 Docker 이미지 설정 파일이다. 여러가지 명령을 토대로 기본 이미지에서 개발환경을 셋팅한 이미지를 생성하는 데에 큰 도움을 받을 수 있을 것 같다.

보통 이렇게들 많이 사용하는 거 같다.

1
2
3
4
5
6
7
8
FROM  ubuntu

RUN apt-get update -y && apt-get install -y \
vim
curl
git

CMD ["/bin/bash"]

간략하게 설명하자면 어떠한 기본 이미지로부터 생성할 것인지를 FROM을 통해 적어준다. 그리고 어떠한 명령어를 실행할 지 RUN에 명시한다.

그렇다면 자주 사용되는 Dockerfile의 커맨드에 대해서 알아보자.

  • FROM
    사용할 기본 이미지를 적어준다. ubuntu:tags 처럼 태그가 표기된 버전도 사용 가능하다. 태그를 생력하면 latest를 사용한 것으로 간주하며 Dockerfile의 첫 번째 설정으로 명시되어야 한다.

  • MAINTAINER
    문자열을 “Author” 메타데이터로 설정한다. 이미지의 maintainer 이름, 연락처와 같은 성세 정보들을 설정하는 데 사용한다. 꼭 입력할 필요는 없지만 Docker hub등에 이미지 공개를 염두한다면 적어주는 것이 좋다.

  • RUN
    이미지 내부에서 수행될 쉘 커맨드를 작성한다.

  • ENV
    이미지 내부의 환경 변수들을 설정한다.

  • USER
    기본적으로 도커는 컨테이너 내에서 모든 프로세스를 root 계정으로 실행한다. 하지만 USER 라인을 통해서 바꿔줄 수 있다.

  • ENTRYPOINT
    컨테이너의 작동이 시작되었을때 (docker run / docker start) 지정된 스크립트 또는 명령을 실행한다. Dockerfile에서 딱 한번만 사용할 수 있다. 주어진 모든 인자들을 해석하기 전에 변수와 서비스들을 초기화하는 “시작 스크립트”를 제공하기 위해 사용하곤 한다.

  • CMD - 데몬 실행
    컨테이너가 시작되는 시점에 실행할 스크립트 혹은 명령어를 작성한다. 만약 ENTRYPOINT가 Dockerfile에 정의되어 있다면 CMD는 Dockerfile의 ENTRYPOINT의 인자로 해석된다. 또한 docker run의 이미지 이름 다음에 오는 인자의 의해서 재정의된다.(마지막 CMD 설정만이 유효하며 이전의 모든 CMD 설정들은 모두 재정의된다.)

  • VOLUME
    볼륨으로 마운트할 특정 파일이나 디렉토리를 정의한다. 컨테이너가 시작도리 때 볼륨으로 해당 파일이나 디렉터리가 복사된다. 만약 여러 개의 인자가 들어갔다면 여러 개의 불륨으로 인식한다.

  • WORKDIR
    RUN / CMD / ENTRYPOINT / ADD / COPY 설정에서 사용할 디렉터리를 의미한다. 상대경로도 사용 가능하다.

Dockerfile을 통한 이미지 생성

1
2
docker build -t [new_image_name] [Dockerfile_path]
docker build -t test-dockerfile-image . # 해당 경로에 자동으로 Dockerfile을 찾아준다
Share

기초적인 Docker 사용법

docker 기본

도커는 컨테이너 기반의 오픈소스 가상화 플랫폼이다. 기존의 가상화 방식인 OS 가상화가 아닌 프로세스를 격리하는 방식이다. OS 가상화와 같은 전/반 가상화 방식은 추가적인 운영체제를 설치해야하기 때문에 성능상의 손실이 크게 발생한다. 그러나 도커의 경우 기존에 리눅스에 존재하던 cgroup과 lxc를 개량하여 프로세스를 격리하는 방식을 취하기 때문에 성능상의 손실이 전가상화 보다 적은 편이다.

위에서 언급한 장점과 또 다른 여러 장점들을 포함해 DevOps개념이 퍼지게 되면서 도커의 대중성이 커지게 되었다고 생각한다.

명령어

이미지 목록 / 삭제

1
2
docker images
docker rmi <image_name>

현재 도커에 설치되어 있는 이미지들을 확인할 수 있다. 또한 rmi 명령어를 통해 이미지를 삭제할 수 있다.

이미지 검색

1
docker search <iamge_name>

docker hub에 있는 이미지들을 검색한다.

컨테이너 생성

1
docker create -it --name <container_name> <image_name>

컨테이너를 생성하는 방법에는 생성과 동시에 실행하는 docker 명령어와 컨테이너를 생성만 하는 docker create 명령어가 있다. 단지 컨테이너를 만들기만 할뿐 실행을 하지는 않기 때문에 demon으로 돌리는 -d 옵션은 존재하지 않는다.

컨테이너 출력

1
2
docker ps
docker ps -a

현재 실행중인 이미지들을 출력한다. 옵션으로 -a을 주게되면 죽어있는 프로세스도 볼 수 있다.

컨테이너 실행

1
2
docker run 
docker run -dit --name <alias_name>

도커 이미지를 실행한다. 첫 번째줄과 같이 실행을 하게 되면 실행과 동시에 프로세스가 죽게되며 docker ps -a명령어를 통해 확인할 수 있다. 옵션을 하나씩 살펴보면 d는 데몬으로 실행함을 의미하며 i는 인터렉티브 모드로 실행함을 t는 tty를 실행할 것임을 의미힌다. 또한 뒤의 –name는 alias 처럼 별칭을 주는 역할을 한다.

-d 옵션을 주면 백그라운드에서 데몬이 돌기 때문에 docker run -dit로 실행하게 되면 bash를 실행해도 바로 shell이 뜨지 않는다. runcreate 명령어의 차이를 알아보자.

  1. create
    docker pull(이미지가 없을때) > docker create
    docker create 명령어는 도커 이미지를 pull한 뒤에 컨테이너를 생성만 할뿐 start / attach 를 실행하지 않는다. 보통 컨테이너를 생성함과 동시에 시작하기 때문에 run 명령어를 더 자주 사용한다.

  2. run
    docker pull(이미지가 없을때) > docker create > docker start > docker attach -it
    docker run 명령어는 컨테이너를 생성과 동시에 실행하고 attach 하기 때문에(-it 옵션) 컨테이너 내부로 들어간다.

컨테이너 접속

1
2
docker attach <name>
exit

생성한 컨테이너 안으로 들어간다. 에 해당하는 부분에는 위에서 지어준 별칭을 통해서도 접근이 가능하다. 접속한 컨테이너로 부터 나오고 싶으면 exit를 입력하면 된다. 여기서 알고 넘어가야 할 것은 attach를 통해 컨테이너에 접속후 exit를 입력해서 컨테이너를 빠져 나오게되면 프로세스가 자동으로 죽는 다는 것이다. 그리고 docker ps를 통해 상태를 확인해보면 COMMAND 항목이 있는데 attach로 컨테이너에 들어갔을때 실행되는 환경이다. 예를 들면 nodejs를 pull 해올 경우 COMMAND가 /bin/bash가 아니라 node환경이기 때문에 docker attach 명령어를 통해 컨테이터에 접속할 경우 bash창이 아닌 js를 실행할 수 있는 nodejs 환경을 볼 수 있을 것이다.

컨테이너 접속(2)

1
2
3
docker start <name>
docker exec -it <name> bash
docker exec -it <name> echo hello world # hello world 출력

보통 컨테이너에 접속하게 될 때, attach 보다는 start를 이용하는 편이라고 한다. 우선 start 명령어를 통해 도커 컨테이너를 실행시켜 놓고 exec 명령어로 interactive / tty 옵션을 주어서 bash 쉘로 접속하는 방법이다. exec 명령어를 통해 컨테이너에 접속을 하게되면 exit로 나갈경우에도 컨테이너가 종료되지 않고 계속해서 남아있다. exec 명령어는 단순히 말 그대로 적혀있는 명령어를 한 번 실행할 뿐이다. 보통 bash 쉘을 접속하기 위해 bash를 적는 편이지만 계속해서 실행할 필요없이 한 번의 커맨드만 필요로 한다면 exec를 사용하면 된다.

컨테이너 검사

1
docker inspect <name>

해당 컨테이너의 설정(path, ip) 값들을 확인할 수 있다.

이미지 생성

1
docker commit <from_container> <to_image_name>

나만의 도커 이미지를 생성할 수 있다.

컨테이너 실행 / 중지

1
2
docker start <container_name>
docker stop <container_name>

이미지로 생성된 도커의 컨테이너를 실행하고 중지한다. stop으로 중지시킨 컨테이너는 일반적인 ps 명령어를 통해 볼 수 없으며 docker ps -a 통해 출력할 수 있다.

컨테이너 삭제

1
docker rm <container_name>

생성된 컨테이너를 삭제한다. 다만, 컨테이너가 실행되지 않은 상태에서만 일반적으로 사용 가능하며 강제로 지우는 방법은 따로 존재한다.

Dockerfile

Dockerfile은 도커 이미지를 생성하기 위한 설정 파일이다. Dockerfile에 설정한대로 이미지를 편리하게 생성하는 데 도움을 준다.

Share