웹 로그 수집을 위한 access.log 증분 수집 스크립트

Posted by taeho Tae-Ho
2017.04.26 12:32 나의 일

로그 수집 이슈

서버 HW, 운영체제 그리고 서버에서 실행되는 웹 서버 등의 애플리케이션의 상태와 해킹 시도 등을 탐지하기 위해 각종 로그를 수집하는 것은 서비스 상태의 모니터링과 보안관제 등의 업무에서 필수적인 요소다.

그래서 ESM(Enterprise Security Management) 솔루션은 물론이고 SIEM과 보안관제 등의 솔루션에서 서버에 쌓이는 로그파일을 수집하는 기능을 제공하며 로그를 수집하기 위해 Syslog 를 이용하는 등 다양한 방법을 사용할 수 있다. 하지만 syslog나 snmp 등을 지원하지 않는 웹서버 로그나 DB로그 그리고 인하우스 애플리케이션의 로그는 별도로 프로그램을 개발하거나 유료SW를 설치하지 않으면 수집하지 못하는 경우가 많다.


특히나 로그를 수집하는 로그 수집 및 분석 솔루션들은 돈을 더~ 벌기위해 로그를 수집하는 간단한 프로그램을 유료로 판매하는 경우가 많다. 하지만 대부분 FTP나 SFTP를 통해 지정된 로그파일을 다운로드 받을 수 있는 기능을 제공하는 경우가 많은데 이 때 사용할 수 있는 간단한 로그 수집 스크립트를 Perl로 작성해보았다.


로그 수집 스크립트를 작성할 때 고려해야할 점은 몇가지가 있는데....


  • 증분 수집이 가능할 것. 
  • 로그 로테이션에 대응 가능할 것.
  • 읽어야할 로그파일이 존재하지 않거나 접근 권한이 없을 경우 에러에 대비할 것 
  • 로그파일의 용량이 클 경우 성능 문제 해결


이정도는 기본적으로 대비하여야 한다



로그의 증분 수집

아마도 많은 분들이 "증분 백업" 이라는 말을 들어보았을 것이다. 특정 시점에 데이터를 백업한 뒤 해당 시점 이후에 쌓인 데이터만 백업하는 것을 "증분 백업"이라고 한다. 로그를 수집할 때도 마찬가지 요건이 지켜져야 한다. 로그파일은 특정 시점 혹은 특정 용량에 도달할 때 까지 하나의 파일에 계속 추가 기록된다. 만약 한번 로그파일을 읽어간 뒤 일정 시간 뒤 다시 전체를 읽어가면 로그 수집 및 분석 솔루션 입장에서는 동일한 로그를 두번 읽어가는 현상이 발생하게 된다.


따라서 로그를 한번 읽어가면 어디까지 읽었는지를 기억했다가 다음번에 로그를 읽을 때 그 이후의 로그만 읽어가야 한다. 이러한 요건이 로그의 증분 수집 요건이다.


로그 로테이션의 대응

로그를 기록하는 거의 모든 SW나 장비들은 로그를 기록하며 일정 기간 혹은 일정 용량에 도달하면 로그파일을 다른 이름으로 변경하고 새로운 로그파일을 생성하여 기록한다. 이때는 이전에 읽었던 로그파일의 위치는 의미가 없어지게 되며 새로운 로그파일의 첫번째 로그부터 모두 읽어가야 한다.


로그 수집 스크립트는 이러한 상황에 대응이 되어야 한다.


로그파일 접근 에러에 대비

읽어야할 로그파일이 없거나 로그파일에 접근 권한이 없을 경우 에러가 발생할 수 있으며 이러한 상황에 대비할 수 있도록 코드가 짜여져야 한다.


로그파일의 용량이 클 경우 성능 문제 해결

읽어야할 로그파일이 클 경우 로그파일을 오픈한 뒤 이전에 마지막으로 읽어간 로그의 행을 찾아가는 과정에서 CPU 사용율이 증가하고 시스템이 느려지는 문제가 발생할 수 있다. 로그파일의 크기가 작을 경우는 전혀 문제가 되지 않지만 로그파일이 커질 수록 CPU 사용율은 점점 높아져 시스템에 문제를 야기할 수 있다.



로그파일 수집 스크립트의 작성

위의 로그파일 수집 이슈에 대응할 수 있는 스크립트를 만드는 방법은 매우 다양하다. 순수하게 Shell Script로 작성할 수도 있고 폼나게 C언어로 작성할 수도 있다. 하지만 현실적으로 가장 효과적인 방법은 Perl이나 Python 등을 이용해 스크립트를 작성하는 방법이다. 


여기서는 Perl을 이용해 작성하였다.


작성된 스크립트는 다음과 같다. 설명도 함께 달아두었다.


----------------------------------------------------------------


#!/usr/bin/perl

use strict;

use warnings;


my $CFGFILE = "CFG파일의 경로";

my $RFILE = "";

my $DFILE = "";

my $DSTDIR = "";

my $FILESFILE = "";

my ($rFile, $dFile, $cfgFile);

my $line;

my $ct = 0;

my $lct = 0;

my @tmpArr;

my @tmpStr;

my $strFile;

my (@srcFileName, @srcFileLine, @srcSvcName);


# CFG 파일을 연다.

open(cfgFile, "<$CFGFILE") or die("ERROR : Could not open logmon.cfg.");

while ($line = <cfgFile>) {

        if ( substr($line,0,1) ne "#") {

@tmpArr = split (':', $line);

# 읽은 파일을 저장할 경로

if ($tmpArr[0] eq "DSTDIR") {

  $DSTDIR = $tmpArr[1];

# 읽을 파일의 목록이 담긴 파일명 및 경로

} elsif ($tmpArr[0] eq "FILESFILE") {

  $FILESFILE = $tmpArr[1];

}

}

}

@tmpArr = ();


close(cfgFile);


# 읽을 파일의 목록이 담긴 파일을 연다.

open(cfgFile, "<$FILESFILE") or die("ERROR : Could not open files.cfg.");


while ($line = <cfgFile>) {


        if ( substr($line,0,1) ne "#") {


@tmpArr = split(':', $line);

# 읽을파일명:서비스명|현재까지읽은라인번호

                $srcFileName[$ct] = $tmpArr[0];

$srcSvcName[$ct] = $tmpArr[1];

$srcFileLine[$ct] = $tmpArr[2];


                $ct = $ct + 1;

        }

@tmpArr = ();

}

close(cfgFile);


# 현재날자와 시간을 구함. DSTDIR에 쓸 때 파일명에 날짜시간을 붙임.

my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime;

my $now = sprintf("%04d%02d%02d_%02d%02d%02d", $year + 1900, $mon + 1, $mday, $hour, $min, $sec);


$ct = 0;

$lct = 0;

chomp $DSTDIR;

chomp $FILESFILE;


# 읽을 파일명을 차례대로 루핑

foreach $RFILE ( 0..$#srcFileName ) {


  # 파일경로및파일명에서 파일명만 추출함.

@tmpArr = split('/', $srcFileName[$RFILE]);

  # 추출한 로그를 쓸 파일명을 기록

$DFILE=$DSTDIR . "/" . $tmpArr[$#tmpArr] . "_" . $srcSvcName[$RFILE] . "_$now";


  # 파일이 없으면 에러 처리

if ( -f $srcFileName[$RFILE] ) {

    # 읽을 파일의 현재 전체 라인수를 구함

$lct = `wc -l $srcFileName[$RFILE] | awk '{print \$1}'`;

    # 전체라인수에서 이전까지 읽은 라인수를 뺌. 즉 추출해야하는 라인수(끝에서 부터)

        $ct = $lct - $srcFileLine[$RFILE];


# tail 명령의 조합.

        $strFile = "tail -" . $ct . " " . $srcFileName[$RFILE];


     # 읽어야할 라인이 있으면...

        if ( $ct > 0 ) {

                $strFile = "tail -" . $ct . " " . $srcFileName[$RFILE];

                `$strFile >> $DFILE`;

# 파일 로테이션 되었으면 

} elsif ( $ct < 0 ) {

                $lct =~ s/\n//g;

                $strFile = "tail -" . $lct . " " . $srcFileName[$RFILE];

                `$strFile >> $DFILE`;

}

} else {

$lct = "0";

        }

 

chomp $lct;

  # 읽어야할 파일목록에 현재까지 읽은 라인 수 기록

open($cfgFile, ">>$FILESFILE.new") or die("ERROR : Could not open file - $FILESFILE.new -.");

        print $cfgFile "$srcFileName[$RFILE]:$srcSvcName[$RFILE]:$lct\n";

        close($cfgFile);


}

# 새로운 읽어야할 파일목록이 저장된 파일교체 (읽은 행수 반영) 

unlink $FILESFILE;

rename "$FILESFILE.new" , $FILESFILE;


---------------------------------------------------------------------------


이렇게 작성된 스크립트는 Crontab 에 등록하여 5분 간격 혹은 그 이상의 주기로 정기적으로 실행하도록 하면 된다.

신고
이 댓글을 비밀 댓글로