Programming/PHP

[Codeigniter 3.x] 쿼리 로그 남기기 #1

minarae7 2023. 1. 27. 22:57
728x90
반응형

개발을 하다가보면 로그의 중요성은 아무리 말로 설명해도 부족하기만 하다. 몸소 체험하고 왜 로그가 중요한지 절실하기 느껴봐야 어떤 로그를 어떻게 남겨야 하는지 느낄 수 있게 된다.

웹 개발에서 있어서 Query에 관련된 로그는 필수적이라고 볼 수 있을 것이다. 어떤 쿼리를 던졌고 해당 쿼리를 처리하는데 걸린 시간이 얼마인지 알 수 있다면 사이트 혹은 프로그램이 느려지는 구간을 찾는데 많은 도움을 받을 수 있게 된다.

해당 글에서는 Codeigniter 3.x 버전에서 쿼리를 남기는 코드를 남겨보고자 한다.

기본적으로 Codeigniter 3.x 버전의 디렉토리 구조를 파악하고 있다는 전제로 하고 설명을 시작한다.

우선 /application/config/hooks.php 파일을 열어서 다음에 코드를 입력한다.

<?php
$hook['post_system'][] = [
    'class'     => 'LogQueryHook',
    'function'  => 'log_queries',
    'filename'  => 'LogQueryHook.php',
    'filepath'  => 'hooks'
];

CI의 Hook을 이용해서 프로그램이 동작을 마치고 response를 보내기 전에 마지막으로 로그를 남길 내용을 지정한다. post_system은 controller의 동작이 마치고 나서 실행할 class와 method를 지정하도록 한다.

filepath로 파일이 위한 디렉토리를 지정하고 filename을 지정해서 어떤 파일을 열지 정하도록 한다. 여기서는 hooks로 지정했는데 이렇게 지정하면 /application/hooks로 지정이 되고 해당 경로에서 filename으로 지정된 LogQueryHook.php를 찾게 된다.

filename으로 지정한 LogQueryHook.php에서 열어서 class로 지정된 LogQueryHook의 function log_queries롤 호출하게 되는 것이다.

그럼 이제 실제로 쿼리를 로그로 남기게 될 코드를 작성하도록 한다.

위에서 작성한 경로 /application/hooks/LogQueryHook.php로 파일을 생성하고 편집을 시작한다.

우선 껍데기는 다음과 같다.

<?php
class LogQueryHook
{
    function log_queries()
    {
    }
}

이렇게 작성하면 config에서 설정한 내용은 채워 둔 셈이 된다.

이제 내용을 채워보도록 한다.

function log_queries()
{
    $ci =& get_instance();

	// 디비의 실행 시간 리스트를 가져옴
    $time = $ci->db->query_times;
    $output = '';
    // 실행한 쿼리 리스트를 가져옴
    $queries = $ci->db->queries;
}

가장 첫 줄에서 $ci 변수에 CI controller의 instance를 남는다.

이렇게 하면 현재 코드에서 실행된 controller instance를 불러 올 수 있게 된다.

그 다음에 쿼리 실행 시간 리스트와 실제로 구동된 쿼리 리스트를 가지고 온다.

CI Contoller instance 안에 db 객체에 query_times와 queries라는 배열 안에 해당 내용을 저장하고 있다.

$output 변수는 추후에 로그에 남길 내용을 저장하게 된다.

function log_queries()
{
    //.... 이전 코드 생략
    if (count($queries) > 0) {
        foreach ($queries as $key => $query) {
            $output .= "[times:{$time[$key]}]\n";
            $output .= $query . "\n";
            $output .= "========================================================================\n";
        }
    }

    if (empty($output) === true) {
        return;
    }
}

만약에 쿼리를 던진 내용이 있다면 로그를 만들기 시작한다. $output으로 출력하는 내용은 각자 지정하면 되지만 여기서는 첫줄에 실행 시간을 남기고 그 다음 줄에 쿼리를 넣고 마지막에 구분자를 삽입하였다.

그리고 만약 쿼리 내용이 없어서 $output이 비어있다면 이후 코드를 실행하지 않고 종료한다.

function log_queries()
{
    // .... 이전 코드 생략
    $ci->load->helper('file');

    $config_log_path = APPPATH . "logs/";
    $date = date("Ym");

    $dir = $config_log_path . 'QueryLog' . $date;

    // 쿼리를 남길 디렉토리가 없다면 디렉토리를 생성
    if (file_exists($dir) === false) {
        @mkdir($dir);
        chmod($dir, 0755);
    }
    // 접속한 ip를 불러와서 로그에 포함
    $ip = $ci->input->ip_address();

    // 파일은 일단위로 지정(현재 날짜)
    $filepath = $dir . '/Query-log-' . date('Ymd') . '.php';

    // 최종 로그 메시지 기록
    $msg = "[" . date("Y-m-d H:i:s") . "][" . $ci->router->class . "/" . $ci->router->method . "]\n";
    $msg .= $output . "\n\n";
    $msg .= "IP_ADDRESS:" . $ip . "\n";
    $msg .= "==========================\n\n";

    // 로그 파일을 append 모드로 열어서 작성한 기록을 남기기
    $handle = fopen($filepath, "a+");
    flock($handle, LOCK_EX); // 로그 파일을 기록하는 동안 해당 파일에 Lock을 걸음
    fwrite($handle, $msg);
    flock($handle, LOCK_UN); // 로그 기록을 마치면 Log을 해제
    fclose($handle);         // 파일 닫기
}

이제 파일에 내용을 정리하여야 한다.

파일을 남길 경로를 지정하고 디렉토리가 없으면 생성하도록 한다. 해당 코드에서는 쿼리 로그를 /application/logs/QueryLog202301의 형태로 월별로 디렉토리를 생성하여서 쿼리를 모아두도록 하였다. 해당 경로와 다르게 로그를 남기려면 $dir에 해당 경로를 지정하면 된다.

디렉토리가 없으면 디렉토리를 먼저 생성하도록 하고 해당 디렉토리 안에 날짜별로 로그 파일을 생성하도록 지정한다.

해당 코드에서는 로그를 남길 때 로그를 남기는 시간과 Router 정보, 접속한 사용자의 IP 정보를 함께 남기도록 메시지를 구성하였다.

마지막으로 메시지를 구성을 마치면 로그 파일을 fopen으로 열어서 로그를 남기도록 한다. 이 때 다른 프로세스와 같은 파일을 열어서 내용이 누락되는 경우를 방지하기 위해서 로그를 파일에 쓰기 전에 해당 파일을 다른 프로세스에서 접근하지 못하도록 파일 Lock을 걸고 기록이 마무리되면 Lock을 풀도록 한다.

해당 코드에서는 일단위로 로그 파일이 생성되도록 하였는데 접속양이 많으면 시간 단위로 파일을 분리해서 남기는 것도 좋은 방법일 것이다.

아래는 해당 파일의 최종 버전이다.

<?php
class LogQueryHook
{
    function log_queries()
    {
        $ci =& get_instance();
        $time = $ci->db->query_times;
        $output = '';
        $queries = $ci->db->queries;

        if (count($queries) > 0) {
            foreach ($queries as $key => $query) {
                $output .= "[times:{$time[$key]}]\n";
                $output .= $query . "\n";
                $output .= "========================================================================\n";
            }
        }

        if (empty($output) === true) {
            return;
        }

        $ci->load->helper('file');

        $config_log_path = APPPATH . "logs/";
        }$date = date("Ym");

        $dir = $config_log_path . 'QueryLog' . $date;

        if (file_exists($dir) === false) {
            @mkdir($dir);
            chmod($dir, 0755);
        }
        $ip = $ci->input->ip_address();

        $filepath = $dir . '/Query-log-' . date('Ymd') . '.php';

        $msg = "[" . date("Y-m-d H:i:s") . "][" . $ci->router->class . "/" . $ci->router->method . "]\n";
        $msg .= $output . "\n\n";
        $msg .= "IP_ADDRESS:" . $ip . "\n";
        $msg .= "==========================\n\n";

        $handle = fopen($filepath, "a+");
        flock($handle, LOCK_EX);
        fwrite($handle, $msg);
        flock($handle, LOCK_UN);
        fclose($handle);
    }
}
728x90
반응형