본문 바로가기

DevOps/Monitoring

[AWS/RDS] RDS MySQL 슬로우쿼리 모니터링 -> 슬랙 알림

 

진행 순서

1. RDS 파라미터 변경

2. RDS 느린 쿼리 로그 내보내기 설정

3. Lambda 함수 생성(1) - cloud watch log 테스트

4. Lambda 함수 생성(2) - log 파싱 후 슬랙 전송

5.  CloudWatch 로그 그룹 > 구독 필터 생성

 

 

RDS 파라미터 변경

- slow_query_log 1로 변경

슬로우쿼리 저장하겠다.

- long_query_time 5로 변경

5초 이상부터 슬로우쿼리다.

- log_output FILE로 변경

TABLE로 설정하면 mysql.slow_query 테이블에 저장되고

FILE로 해야 cloud watch 로그 그룹에 파일이 생성됩니다.

파라미터를 변경해 준 후 파라미터 그룹을 변경해줍니다. DB 인스턴스 재부팅 해줘야 반영됩니다.

 

 

RDS 느린 쿼리 로그 내보내기 설정

인스턴스 설정에서 로그 내보내기에 느린 쿼리 로그를 선택합니다.

그러면 아래와 같이 cloud watch 로그 그룹이 추가됩니다.

로그 스트림을 열면 아래와 같이 슬로우쿼리 로그가 쌓여있습니다.

 

 

Lambda 함수 생성(1) - cloud watch log 테스트

해당 람다는 쌓인 로그를 파싱해서 슬랙으로 전송하는 역할을 합니다.

람다 코드 작성 전 간단한 코드로 cloudwatch log에서 넘어오는 데이터를 확인합니다.

var zlib = require('zlib');
exports.handler = function(input, context) {
    var payload = Buffer.from(input.awslogs.data, 'base64');
    zlib.gunzip(payload, function(e, result) {
        if (e) { 
            context.fail(e);
        } else {
            result = JSON.parse(result.toString('ascii'));
            console.log("Event Data:", JSON.stringify(result, null, 2));
            context.succeed();
        }
    });
};

cloudwatch에서 보내주는 로그는 Base64로 인코딩되고 gzip 형식으로 압축되어 있기 때문에 파싱하기 위해서는 gzip 압축을 풀고Base64로 디코딩을 해야 합니다.

 

템플릿을 cloudwatch-logs로 선택하고 테스트를 진행해봅니다.

테스트 결과를 보면 로그가 어떤 형태로 보내지는지 알 수 있습니다.

{
  "messageType": "DATA_MESSAGE",
  "owner": "123456789123",
  "logGroup": "testLogGroup",
  "logStream": "testLogStream",
  "subscriptionFilters": [
    "testFilter"
  ],
  "logEvents": [
    {
      "id": "eventId1",
      "timestamp": 1440442987000,
      "message": "[ERROR] First test message"
    },
    {
      "id": "eventId2",
      "timestamp": 1440442987001,
      "message": "[ERROR] Second test message"
    }
  ]
}

이런 형태로 오는 것을 알 수 있고

logEvents의 message에 저희 로그 메세지를 넣고 테스트 해봅니다.

{"messageType":"DATA_MESSAGE","owner":"123456789123","logGroup":"testLogGroup","logStream":"testLogStream","subscriptionFilters":["testFilter"],"logEvents":[{"id":"eventId1","timestamp":1440442987000,"message":"# Time: 2022-08-18T01:04:30.682656Z
# User@Host: root[root] @  [xx.xx.xx.xx]  Id:    32
# Query_time: 7.000176  Lock_time: 0.000000 Rows_sent: 1  Rows_examined: 1
SET timestamp=1660784663;
/* ApplicationName=IntelliJ IDEA 2021.2.2 */ select sleep(7);"}]}

gzip으로 압축한 테스트 데이터는 https://www.multiutil.com/text-to-gzip-compress/ 해당 사이트에서 생성합니다.

gzip value를 test data에 넣어줍니다.

메세지가 잘 들어온 것을 확인할 수 있습니다.

 

 

Lambda 함수 생성(2) - log 파싱 후 슬랙 전송

데이터 파싱코드는 52line.

index.js

const ENV = process.env
if (!ENV.webhook) throw new Error('Missing environment variable: webhook')

const SLACK_URL = ENV.webhook;
const https = require('https');
const zlib = require('zlib');
const SLOW_TIME_LIMIT = 7; // 7초이상일 경우 슬랙 발송


exports.handler = (input, context) => {
    var payload = Buffer.from(input.awslogs.data, 'base64');
    zlib.gunzip(payload, async(e, result) => {
        
        if (e) { 
            context.fail(e);
        } 
        
        const resultAscii = result.toString('ascii');
        
        let resultJson;

        try {
            resultJson = JSON.parse(resultAscii);
        
        } catch (e) {
            console.log(`[알람발송실패] JSON.parse(result.toString('ascii')) Fail, resultAscii= ${resultAscii}`);
            context.fail(e);
            return;
        }
        
        console.log(`result json = ${resultAscii}`);

        for(let i=0; i<resultJson.logEvents.length; i++) {
            const logJson = toJson(resultJson.logEvents[i], resultJson.logStream);
            console.log(`logJson=${JSON.stringify(logJson)}`);
        
            try {
                const message = slackMessage(logJson);
                
                if(logJson.queryTime > SLOW_TIME_LIMIT) {
                    await exports.postSlack(message, SLACK_URL);
                }    
            } catch (e) {
                console.log(`slack message fail= ${JSON.stringify(logJson)}`);
                return;
            }

        }
    });
};

function toJson(logEvent, logLocation) {
    const message = logEvent.message;
    
    const currentTime = toYyyymmddhhmmss(logEvent.timestamp);
    const queryTimeRegex = new RegExp('Query_time: ([^&@]+)  Lock_time');
    const queryTime = message.match(queryTimeRegex)[1];
    const queryRegex = new RegExp('select ([^&@]+)|update ([^&@]+)|delete ([^&@]+)|insert ([^&@]+)');
    const query = message.match(queryRegex)[0];
    
    return {
        "currentTime": currentTime,
        "logLocation": logLocation,
        "queryTime": queryTime,
        "query": query
    }
}

// 타임존 UTC -> KST
function toYyyymmddhhmmss(timestamp) {

    if(!timestamp){
        return '';
    }

    function pad2(n) { return n < 10 ? '0' + n : n }

    var kstDate = new Date(timestamp + 32400000);
    return kstDate.getFullYear().toString()
        + '-'+ pad2(kstDate.getMonth() + 1)
        + '-'+ pad2(kstDate.getDate())
        + ' '+ pad2(kstDate.getHours())
        + ':'+ pad2(kstDate.getMinutes())
        + ':'+ pad2(kstDate.getSeconds());
}

function slackMessage(messageJson) {
    const title = `[${SLOW_TIME_LIMIT}초이상 실행된 쿼리]`;
    const message = `언제: ${messageJson.currentTime}\n로그위치: ${messageJson.logLocation}\n실행시간: ${messageJson.queryTime}초\n쿼리: ${messageJson.query}`;
    
    return {
        attachments: [
            {
                color: '#2eb886',
                title: `${title}`,
                fields: [
                    {
                        value: message,
                        short: false
                    }
                ]
            }
        ]
    };
}

exports.postSlack = async (message, slackUrl) => {
    return await request(exports.options(slackUrl), message);
}

exports.options = (slackUrl) => {
    const {host, pathname} = new URL(slackUrl);
    return {
        hostname: host,
        path: pathname,
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
    };
}

function request(options, data) {

    return new Promise((resolve, reject) => {
        const req = https.request(options, (res) => {
            res.setEncoding('utf8');
            let responseBody = '';

            res.on('data', (chunk) => {
                responseBody += chunk;
            });

            res.on('end', () => {
                resolve(responseBody);
            });
        });

        req.on('error', (err) => {
            console.error(err);
            reject(err);
        });

        req.write(JSON.stringify(data));
        req.end();
    });
}

lambda 구성 > 환경 변수 추가

 

 

CloudWatch 로그 그룹 > 구독 필터 생성

 

 

결과

 

참고

https://mozi.tistory.com/483

 

[AWS] AWS Aurora MySQL 지연쿼리 수집하는 방법

AWS Aurora MySQL 지연쿼리 수집하는 방법 AWS 에서는 Aurora MySQL 의 지연쿼리를 2가지 방법으로 수집할 수 있습니다. ( 더 있으려나.. ? ) 첫 번째는, mysql 의 slow_log 테이블에 수집하는 방법입니다. 두 번.

mozi.tistory.com

슬로우쿼리 로그 파라미터 설정 

https://jojoldu.tistory.com/570

 

PostgreSQL RDS Slow 쿼리 Slack으로 알람 보내기

서비스를 운영하다보면 여러가지 이유로 서버 장애가 발생합니다. 그 중 가장 빈도수가 높은 원인은 DB의 슬로우쿼리일텐데요. 어떤 쿼리가 언제, 얼마나 긴시간동안 수행되었는지에 대해 알람

jojoldu.tistory.com

람다 테스트

https://hyunki1019.tistory.com/151

 

[AWS] AWS ELK로 RDS Slowquery 모니터링

안녕하세요. RDS Slow query 를 수집하기 위해서 알아보던 도중(예전에 김종열 팀장님 도움으로 python으로 수집하던 소스는 있지만, UI로 보여주고 싶은 마음에) ELK 를 이용하면 제가 원하는 화면을

hyunki1019.tistory.com

데이터 파싱