Overview

필자는 JSON으로 로그를 남긴다는 생각을 해본적이 없었습니다.

원래 하던대로 로그를 텍스트로 남기고 특정 키워드를 검색해내고 그 주변의 로그를 확인 하는 방법으로 주로 사용했기 때문입니다.

    cat webserver-log-2019-07-18.log | grep "@@logType=ERROR"
이렇게 많이 사용했습니다.

JSON으로 로그를 남기는것은 어렵지 않지만, 어떻게 검색할지가 가장 큰 문제라고 생각 됩니다.

    cat webserver-log-2019-07-18.log | grep '"logType": "ERROR"'
안되는것은 아니지만 뭔가 불편해 보입니다.

JSON 로그 검색시 위와 같이 명확한 케이스는 어떻게든 처리가 가능하지만, errorMessage값안에 원하는 키워드가 있는 경우만 보고 싶다면 매우 어려워집니다. 문자검색만으로 키와 값을 연결해서 보기 어렵기 때문입니다. 복잡한 요구조건으로 검색하려면 별도의 파싱 엔진을 구축하고 로그를 분석하는 작업을 해야할것입니다. (ex 하둡)

하지만 걱정하지 않아도 됩니다.

AWS많은 서비스들이 JSON포멧을 훌륭하게 지원하기 때문입니다.

AWS Logs

plan
AWS CloudWatch (엄청 좋기 때문에 빤짝이를 넣어봤습니다)

AWS logs는 서비스에서 발생한 로그를 확인하는 서비스입니다. 웹서비스나 서버리스 람다, 배치등 모든 서비스의 로그는 logs에서 확인이 가능합니다.

필자도 logs를 키워드 검색을 하는 방식으로 많이 사용하고있었다. 그나마 최근에 insights를 활용하면서 조금 스마트해진 방식을 쓰고있지만 로그 분석하기가 쉽지는 않았습니다.

logs는 JSON 포멧을 잘 지원합니다. 그리고 의외로 너무 간편해서 놀라게 될것입니다. 아래가 검색하는 방법입니다.

    // logType이 ERROR인 로그 찾아내기
    { $.logType = "ERROR" }
plan
필터를 이용한 JSON 로그 검색 결과

너무 간단해서 설명이 필요 없을정도입니다. 기존의 키워드 검색을 하던 방식에서 $.을 넣으서 검색하면 되는것입니다.

앞서 언급한 복합 케이스는 어떻게 해야할까요? logType 이 ERROR이면서 errorMessage인증실패 라는 문구가 있는 로그를 찾고 싶다면 아래와 같이 검색하면 됩니다.

    // 에러이면서 인증실패라는 키워드를 가진 로그
    { $.logType = "ERROR" && $.errorMessage = "*인증실패*" }
plan

*는 어떤문자도 올수있다는 뜻으로 생각하면 됩니다.

숫자에 대한 수식이 지원하기 때문에 정교한 검색이 가능합니다.

    // 나이가 20세 이상인 유저 로그
    { $.user.age >= 20 }
plan

또한 에러코드와 같이 유형별로 구성된 내용이 있다면 (*)연산자를 이용해 응용이 가능합니다

    // VA01 으로 시작하는 에러 코드 검색
    { $.errorCode = "DA0001*" }
plan
DA00012 와 DA00013이 검색되었다.

https://docs.aws.amazon.com/ko_kr/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html

AWS 공식 문서에서 자세한 내용을 확인 할 수 있습니다.

AWS Insights

insights는 logs보다 좀 더 강력한 로그 검색을 지원합니다.

검색 내용을 그래프로 지원하고, 검색에 포함되는 총 건수도 체크해 주기 때문에 사용 용도가 logs와는 다른면이 있습니다. logs와 insights의 문법이 상이한게 약간 흠이긴 하지만 기능상 차이가 크게 없고 문법도 심플하기 때문에 사용하는데 어려움은 없습니다.

    | filter logType = "ERROR"
plan

logs에서 logType이 ERROR인 필터 조건을 똑같이 구현했다.

insights는 자동완성을 지원하기 때문에 해당 로그가 있는 범위를 검색하면 우측에 사용 가능한 키워드의 가이드 나옵니다. 이점은 매우 편합니다.

plan

마찬가지로 logType 이 ERROR이면서 errorMessage인증실패 라는 문구가 있는 로그를 찾아보겠습니다.

    | filter logType = "ERROR" and errorMessage like /인증실패/
plan

숫자 연산도 거의 동일하게 지원합니다.

    | filter user.age >= 20
plan

insights는 정규식을 지원하니 정규식으로 에러 코드 검색을 할 수 있습니다.

    | filter errorCode like /^DA0001/
plan


이제 로그를 JSON으로 남기기만 하면됩니다!

logs와 insights의 검색 방법을 보고나니 벌써 부터 사용해보고 싶은 생각에 근질 근질 할것입니다. 그렇다면 어떤 방법으로 JSON로그를 남기는것이 좋을지 python코드로 설명해보겠습니다.

    print(json.dumps(json_data))

어려울것 없이 JSON데이터를 출력하면됩니다.

그렇지만 서비스입장에선 logger를 이용하는것이 바람직하므로 python-JSON-logger를 활용해 포멧터를 작성하였습니다. 서비스에 맞는 logger서비스를 이용해서 로그를 남기되 그내용을 JSON을 직렬화시킨 내용이면 되겠습니다.

    import logging
    from pythonjsonlogger import jsonlogger

    logger = logging.getLogger()

    logHandler = logging.StreamHandler()
    formatter = jsonlogger.JsonFormatter()
    logHandler.setFormatter(formatter)
    logger.addHandler(logHandler)


    logger.info({"logType": "ERROR", "errorCode": "DA00012" }) # json내용 생략

https://github.com/madzak/python-json-logger

준비가 끝났다, 이제 모니터링을 만들자

로그를 JSON으로 쌓기 시작했다면 벌써 많은 부분 성공한 것입니다. 로그의 가공이 아주 용이해 졌기 때문에 원하는 결과를 만들기 한결 쉬워집니다.

이제 로그를 모니터링에서 사용하기 편리하게 수치화 해야한다. 예를 들어 “5분 이내 logType이 ERROR인 로그가 5건 이상 발생하면 장애 발생으로 본다.” 이런 정책들입니다.

이를 위해서 logs의 metrics 서비스를 이용하면 됩니다. metrics도 사용이 간편해 어렵지 않게 진행 할 수 있을것입니다.

먼저 로그타입이 에러인 로그를 검출할 지표(metrics)를 생성해줍니다.

먼저 지표(metrics)를 정의합니다.

plan

지표는 생성 시에도 logs에서 검색한 구문과 같은 구문으로 패턴을 지정할 수 있으며, 테스트 로그 데이터를 통해 제대로 해당 패턴이 제대로 검출 되는지 확인할 수 있습니다.

위에서 테스트 로그 데이터는 실제 logs에 있는 로그스트림의 데이터입니다.

위에서 정의한 지표를 이용해서 지표와 필터를 생성 해줍니다

plan

이후 해당 람다 함수 로그 그룹의 필터를 확인해보면 지표 필터가 생성 되어있는 것을 확인 할 수 있습니다.

plan

이렇게 생성된 지표 필터는 CloudWatch에서 지원하는 대시보드에 필터로 등록 할 수 있으며, 로그레벨이 ERROR인(패턴: {$.logLevel = “ERROR”}) 로그를 시각화 하여 한눈에 알아볼 수 있도록 구성 할 수 있습니다.

대시보드 세팅에 관한 글은 곽정섭과장의 글을 참고하세요.

CloudWatch에 대하여

지금까지 만든 지표로 제작한 대시보드를 통해 에러 현황을 한눈에 파악 할 수 있게 되었습니다.

하지만 에러 발생은 최대한 빠른 대응이 생명입니다. 그런데, 대시보드는 개발자가 직접 해당 화면을 열기 전 까지는 에러가 발생했는지 조차 알 수 없습니다. 또한, 대시보드의 자동 갱신 기능을 사용하면서, 해당 조회 구간을 벗어나게 되면 에러가 발생한 것 조차 놓치고 지나칠 수 도 있습니다.

따라서 개발자가 에러가 발생한 그 시점에 최대한 빠르게 인지하기 위해서는 알람이 필수적입니다.

최근 많은 회사에서 사용하고 있는 메신져인 슬랙(Slack)을 이용해 알람을 전송 해보도록 하겠습니다.

모니터링의 꽃은 알람!

먼저 알람을 수신할 SNS 주제(Topic)을 생성해줍니다.

plan

생성된 지표 필터로 돌아가서 경보 생성 버튼을 눌러 알람을 세팅 해보겠습니다.

plan

기본은 5분이지만 필자는 테스트를 위해 1분으로 변경 해주었습니다. (1분 이하는 추가 요금이 발생합니다..)

알람의 조건을 선택해주고 임계값을 입력합니다.

plan

에러가 1개 이상이라도 발생하면 알람이 발생하도록 단일 값으로 “정적(Static)”, 조건으로 “보다 크거나 같음(>= 임계값)”, 임계값 으로 1 을 입력 해주었습니다.

다음을 눌러 작업을 구성해줍니다.

plan

경보상태일 때 Notification을 발생시키기 위해 위에서 트리거로 “경보 상태”, 알림 수신 SNS 에는 이전에 생성한 SNS 토픽의 arn을 입력해줍니다.

마지막으로 알람의 이름과 설명을 입력해주면

plan

성공적으로 경보가 생성되었습니다.

plan

현재 에러 로그가 발생하지 않은 상태로 데이터 부족 상태입니다.

테스트를 진행하기 전 SNS전송이 잘 세팅 되었는지 해당 알람을 눌러 확인해줍니다.

plan

SNS를 감지하여 슬랙으로 메시지를 전송할 Lambda를 생성해줍니다.

필자는 사용중이던 Codestar프로젝트에 python3.7 람다를 추가해주었습니다.

슬랙을 보낼 수 있는 라이브러리인 slacker는 requirement.txt에 추가해주면됩니다.

    # -*- coding: utf-8 -*-
    from slacker import Slacker
    slack_token = "슬랙 토큰"

    def handler(event, context):
        record = event.get('Records')[0]
        sns = record.get('Sns')

        text = '```'
        text = text + '모니터링중 에러가 발생하였습니다.\n제목을 눌러 알람을 확인해주세요.\n[ SNS 내용 ]\n'
        text = text + 'Subject: ' + sns.get('Subject') + '\n'
        text = text + 'Message: ' + sns.get('Message') + '\n'
        text = text + 'Timestamp: ' + sns.get('Timestamp') + '\n'
        text = text + '```'

        attachments_dict = dict()
        attachments_dict['pretext'] = '에러발생'
        attachments_dict['title'] = '개발2팀 에러 모니터링 알람 (보러가기)'
        attachments_dict['title_link'] = '[에러로깅 확인용 주소]'
        attachments_dict['text'] = text
        attachments_dict['mrkdwn_in'] = ["text", "pretext"]  # 마크다운을 적용시킬 인자들을 선택합니다.
        attachments = [attachments_dict]

        # 슬랙 객체 생성 타임아웃 300초
        slack = Slacker(slack_token, timeout=300)

        slack.chat.post_message(channel="#dev2_alarm", text=None, attachments=attachments, as_user=True)

이제 생성한 람다를 호출하도록 SNS 주제(Topic)의 구독(Subscribe)을 생성해줍니다.

plan

주제 ARN에 처음 생성한 SNS Topic의 arn을 입력해주고,

프로토콜을 AWS Lambda로 선택해줍니다.

엔드포인트는 방금 생성한 Lambda의 arn을 입력해줍니다.

(Lambda의 arn은 Lambda 관리화면 상단에 있습니다.)

생성된 구독(Subscribe) 확인

plan

구독(Subscribe)을 생성하고 나면 전에 주제(Topic) 화면 하단에 연결된 구독이 나타나게 됩니다.

plan

자 이제 테스트 에러를 생성해 보겠습니다.

테스트로 에러로그 3개를 생성하여 잠자고 있는 알람을 깨워보았습니다.

plan

알람 발생!

plan

지표 생성 시에 기간을 1분으로 생성하였기 때문에 1Tick 이 1분이 됩니다. 따라서 에러 로그가 발생한 즉시에 경보 상태로 전환되는 것은 아니고, 현재 시점의 다음 Tick (다음1분)동안 발생한 에러 로그가 있을 경우 경보 상태로 전환 되며, 그 다음 Tick 에 로그가 없으면 해제됩니다.

슬랙 메시지 확인

plan

어느 리전의 어떤 알람이 발생했는지 확인 할 수 있으며, Python의 slacker에서 title링크로 해당 로그스트림 url을 넣어뒀기 때문에 제목을 누르면 바로 로그스트림으로 이동하여 상세한 전후 상황을 파악 할 수 있습니다.

에러 알람 모식도

plan

모니터링 담당자를 유기적으로 관리 하기 위해서 SNS를 이용하였습니다. SNS 구독을 생성하고 담당자를 추가 삭제하면 편리하게 사용이 가능하기 때문입니다. 위의 예제 에서는 사내 메신져인 Slack을 이용하였지만, SMS(단문 메시지)와 email도 기본적으로 지원합니다.

위 글에서 슬랙의 토큰이 사용되었는데, 슬랙 토큰발급 방법에 관해서는 곽정섭과장의 글을 참고 하십시요

Node 서버로 Slack 메신저 자동화하기

마치며

이제 정말로 스마트한 로그를 관리 할 수 있게 되었습니다. JSON 기반의 로그는 활용도가 매우 좋고 사용도 편리해 이용하지 않을 이유가 없습니다. 물론 레거시 시스템의 메세지를 한개씩 옮겨가는 작업들은 필요하겠지만 노력에 보답이 될만큼 충분히 가치있는 작업이 될거라 생각합니다.

알람을 보내는것으로 글은 마무리 되지만 JSON로그를 다른 다양한 목적으로 사용하는것은 매우 칭찬할 일~이야 😘

사족

  • insight에서 JSON데이터를 필드로 활용이 가능합니다. (CSV export는 덤)
  • insight에서 사용한 쿼리는 히스토리가 남으며 로컬에 저장도 가능합니다.
    fields @timestamp, user.age, @message
    | filter errorCode like /^DA0001/
    | sort @timestamp desc
    | limit 20
insight 검색에 사용한 쿼리 전문
plan
user.age 라는 필드가 생겼다.
  • logs와 insights모두 날짜형 범위 검색을 지원하지 않습니다.
  • AWS가 기본 지원하는 로그 등록 날짜를 날짜 검색 필터로 삼아야합니다.
  • lambda layer기능을 이용한다면 serverless lambda에 로거를 적용하기 한결 쉬워집니다.

천보성 팀장 | 혁신팀
chunbs@brandi.co.kr
브랜디, 오직 예쁜 옷만