상세 컨텐츠

본문 제목

C# log4net 을 활용한 File Rolling

C#

by 범쥰 2023. 3. 2. 19:43

본문

오늘은 C#으로 Log File(. log)을 열어, 특정 패턴의 정규식에 매칭되는 텍스트 수량이 얼마나 되는지 파악하는 로직에서 버그 개선을 했다.

 

log4net

우선 로그 라이브러리는 log4net을 사용한다. log4net은 .NET Framework를 기반으로 하는 로깅 프레임워크이다. 

log4net에 대한 설명을 아래 공식홈페이지 url로 설명을 생략한다.

https://logging.apache.org/log4net/release/manual/introduction.html

 

Apache log4net – Apache log4net Manual: Introduction - Apache log4net

Apache log4net™ Manual - Introduction Overview This document is based on Short introduction to log4j by Ceki Gülcü. The log4net framework is based on Apache log4j™, see http://logging.apache.org/log4j/ for more information on log4j. The log4net frame

logging.apache.org

 

파일 로깅을 구현하는 방법 중 하나는 rolling file appender를 사용하는 것이다. Rolling file appender는 로그 파일을 일정 크기나 시간 단위로 분할하여 저장한다.

예를 들어, 일일 로그를 유지하고자 하는 경우, 매일 새로운 로그 파일을 만들어서 새로운 로그를 쓸 수 있다. 이렇게 하면 일일 로그 파일을 보존할 수 있다. Rolling file appender를 사용하여 파일 크기를 제한하는 경우, 로그 파일이 너무 커지지 않도록 제한할 수 있다. 로그 파일의 크기 제한은 MaxFileSize 속성을 사용하여 설정할 수 있다. 예를 들어, MaxFileSize가 10MB로 설정되어 있으면 10MB를 초과하지 않는 로그 파일만 유지된다.

log4net에서 RollingFileAppender를 사용하려면, 먼저 log4net.config 파일에서 appender를 정의해야 한다.

다음은 appender에 대한 예제이다.

<log4net>
    <appender name="RollingFileAppender" type="log4net.Log.RollingFileAppender"> <!--로그파일 관리방식 지정 : Rolling File 중 가장 오래된 File을 삭제-->
      <file value=".\Log\" /> <!-- 어느 수준의 로그를 기록할지-->
      <appendToFile value="true" /> <!--기존 파일에 이어쓸것인지 / false 면 프로그램 실행마다 새로운 로그파일만듬-->
      <rollingStyle value="Composite" /> <!--로그 파일 저장 기준 https://logging.apache.org/log4net/release/sdk/html/T_log4net_Appender_RollingFileAppender_RollingMode.htm-->
      <maximumFileSize value="5MB" /> <!--logFile Max Size-->
      <maxSizeRollBackups value="3" /> <!--rolling file max count-->
      <datePattern value="'LOG'_yyyy-MM-dd'.log'" /> <!-- log 파일명 형식 -->
      <staticLogFileName value="false" /> <!--로그파일 유지-->
      <lockingModel type="log4net.Appender.FileAppender+MinimalLock" /> <!--날짜 변경시 기존 로그 파일이름을 어떤식으로 변경할지에 대해서 설정-->
      <layout type="log4net.Layout.PatternLayout"> <!--로그 내용 형식 : 시간 - 로그내용 newLine -->
        <conversionPattern value="%date [%thread] %level %logger | %message%newline" />
      </layout>
    </appender>
    <!--RollingFileAppender가 정의되면, Logger에서 RollingFileAppender를 참조할 수 있다. -->
    <root>
      <level value="Info" /> <!-- logLevel-->
      <appender-ref ref="RollingFileAppender" /><!-- 만든 appender를 사용하겠다고 선언 -->
    </root>
  </log4net>

 

log4net 활용법은 이쯤 해두고, 해당 log File 어떻게 관리해서 정규식을 통한 체크(File Read)와 동시에, File Rolling을 가능하게 하는지에 대해 적어보겠다.


FileStream

FileStream 클래스는 .NET Framework에서 파일 I/O 작업을 수행하기 위해 사용되는 클래스이다. 이 클래스를 사용하면 파일에서 데이터를 읽거나 파일에 데이터를 쓸 수 있다.

FileStream 클래스를 사용하여 파일을 읽거나 쓰기 위해서는 파일 경로와 파일 모드를 지정해야 한다. 파일 경로는 읽거나 쓸 파일의 경로를 지정하는 문자열이다. 파일 모드는 파일을 열 때 사용할 모드를 지정하는 열거형 값이다.

파일 모드는 FileAccess 열거형 값과 FileMode 열거형 값으로 구성된다. FileAccess 열거형 값은 파일에 대한 읽기, 쓰기 또는 모두 지원하는 작업을 지정한다. FileMode 열거형 값은 파일을 열 때 지정할 작업을 지정한다. 예를 들어, 파일을 생성하거나 존재하는 파일을 열고, 파일 끝에 추가하는 등의 작업을 할 수 있다.

아래는 FileStream 클래스를 사용하여 파일을 읽고 쓰는 간단한 예제이다.

using System;
using System.IO;

class Program
{
    static void Main(string[] args)
    {
        string filePath = "C:\\Temp\\test.txt";

        // 파일 생성 및 쓰기
        using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
        {
            byte[] buffer = System.Text.Encoding.UTF8.GetBytes("Hello, world!");
            fs.Write(buffer, 0, buffer.Length);
        }

        // 파일 읽기
        using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
        {
            byte[] buffer = new byte[1024];
            int bytesRead = fs.Read(buffer, 0, buffer.Length);
            Console.WriteLine(System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead));
        }
    }
}

위의 예제에서는 Create 파일 모드로 파일을 생성하고, Write FileAccess 모드로 파일을 쓰는 FileStream 객체를 생성한다. 그리고 "Hello, world!" 문자열을 바이트 배열로 변환하여 파일에 쓴다.

그 후, Open 파일 모드로 파일을 열고, Read FileAccess 모드로 파일을 읽는 FileStream 객체를 생성한다. 그리고 파일에서 읽은 데이터를 바이트 배열로 저장하고, 바이트 배열을 문자열로 변환하여 콘솔에 출력한다.

FileStream 클래스를 사용하여 파일을 읽거나 쓰는 것은 매우 간단하고 효율적이다. 파일을 읽거나 쓰는 작업이 많은 응용 프로그램에서는 FileStream 클래스를 사용하여 파일 I/O 작업을 처리하는 것이 좋다.

 

하지만, 나의 경우엔 해당 예제코드와 같은 FileStream으론 File 이 롤링된 후 새로운 롤링된 파일로 FileStream을 열기 때문에 기존 File의 남은 부분에 대한 Check를 하지 않아 loss 가 발생한다는 것이다.

 

문제점 Loss : rolling 되기 전, 기존 log File 남은 부분에 대한 check 누락

[기존방식]

using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read) -> using 이라 프로그램을 잡고 있지 않기 때문에 로깅하는데 문제없고, 프로그램에서 문제없이 롤링함. -> 로스 존재

 

[수정테스트 1]

FileStream fs = new FileStream(fileFullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite) -> using 아니어도 FileShare 덕분에 감시로그에 해당하는 프로그램이 로깅하는데 문제없지만, 프로그램에서 새로 롤링 파일을 만들지 못함.

 

[수정테스트 2]

롤링되지 않았을 때는 새로운 stream 읽고 나서 close 해버리면 되지 않을까 했는데, 기존 FileStream(​oldFs)에 대한 close 안되고 fileStream을 유지하니까 롤링이 안됨.

FileStream fs = new FileStream(fileFullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite ;

if (oldfs == null)
       oldfs = fs;
else
       fs.Close();

 

FileShare 란 다른 프로세스 또는 쓰레드가 파일에 대해 가지는 액세스 형식을 지정하는 값

FileShare.Read 만 쓰면,  다른 프로세스가 해당 로그파일에 Read O / Write X , 롤링 안됨

FileShare.Write 만 쓰면,  다른 프로세스가 해당 로그파일에 Read X / Write O , 롤링 안됨

 

FileShare에 대한 항목 중 FileShare enum은 아래와 같습니다.
- None
    다른 프로세스가 해당 파일에 접근할 수 없습니다. (내 작업이 끝나야 접근이 가능합니다.)
- Read
   다른 프로세스가 파일을 읽을 수 있습니다.
- Write
   다른 프로세스가 파일을 쓸 수 있습니다.
- ReadWrite
   다른 프로세스가 파일을 읽고 쓸 수 있습니다.
- Delete
   다른 프로세스가 파일을 지울 수 있습니다.
- Inheritable
   자식 프로세스가 파일 핸들을 상속받을 수 있습니다.

 

최종적으로, 파일의 롤링 여부는 기존 파일 생성시각과 새롭게 읽어온 파일 생성시각이 다르다면 롤링되었다고 판단하였고 새롭게 읽어온 파일을 제외하고 파일 경로에서 가장 마지막에 수정된 파일을 롤링되기 직전의 파일이라고 판단하여 loss 없이 계산할 수 있었다.

using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
        // 조회시점마다 경로에 동일한 이름을 가진 파일이 기존에 열어놨던 파일의 생성시각과 다를경우 롤링되었다고 판단
        DateTime newcreateTime = File.GetCreationTime(filePath);
        if (createTime != newcreateTime)
        {
            // 롤링된 후 마지막으로 쓰여진 파일을 다시 읽어옴
            FileInfo latestFile = FindLatestFile(filePath, newcreateTime);
            if(latestFile != null)
            {
                // 기존 파일을 offset 부터 파일끝까지 다 읽음
                using (FileStream oldfs = new FileStream(latestFile.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                {
                    MatchRegex(oldfs, g, ref count);
                }
            }

             // 새로운 파일의 offset 1로 지정
             MoveOffset(fileFullPath, true);
             MatchRegex(fs,g, ref count);

             createTime = newcreateTime;
         }
         else
         {
             if (offset == fs.Length)
             {
                 // 마지막으로 read 하고 파일에 변경점이 없을 경우
                break;
             }
             MatchRegex(fs,g, ref count);
         }
}