출처: https://tech.cygames.co.jp/archives/3401/

GitHub - Cysharp / ZLogger

특히 컨테이너화에서 표준 출력은 중요하다. 예를 들어 Datadog Logs 와 Stackdriver Logging 은 컨테이너의 표준 출력에서 직접 로그를 수집한다. Thw Twelve-Factor App - XI.Logs 섹션에서도 표준 출력으로 내보내기가 추천 되고 있다. 그런 가운데, 로컬 환경용으로 장식이 과다한 로그와 느린 Console.WriteLine로 표준 출력을 취급하는 것은 너무 오래된 아이디어라고 할 수 있으며, 불행히도 지금까지 .NET Core는 여기를 중요 시한 라이브러리는 존재하지 않았다.

기존의 로깅 처리는 많은 낭비가 존재하고 있다.

표준 String.Format은 값을 모두 object로 받기 때문에 박싱이 발생하고, 더욱 UTF16 String을 새로 생성한다. 이 String을 UTF8로 인코딩하여 최종 스트림으로 쓸 흐름이 있지만 ZLogger의 경우 ZString을 사용하여 포맷 문자열을 직접 버퍼 영역에 UTF8로 쓰고, ConsoleStream에 정리해서 흘려 보낸다. 원시 타입의 박싱도 발생하지 않고, 비동기적으로 단번에 쓰기 때문에, 호출 측은, 그리고 애플리케이션 전체에 부하를 주지 않는다.

또한 표준 출력에 최적화 것 외에도 파일로의 쓰기 등의 프로바이더 등의 일반 로거로서 기대 되는 내용도 표준으로 준비하고 있기 때문에 다양한 곳에서 지금 사용할 수 있다.

로거 설정은 Generic Host에 따르면 ConfigureLogging에 따른다. 따라서 Microsoft.Extensions.Logging 필터링 등을 설정할 수 있다.

using ZLogger;

Host.CreateDefaultBuilder()
   .ConfigureLogging(logging =>
   {
       
// optional(MS.E.Logging):clear default providers.
       logging.ClearProviders();

       
// optional(MS.E.Logging): default is Info, you can use this or AddFilter to filtering log.
       logging.SetMinimumLevel(LogLevel.Debug);

       
// Add Console Logging.
       logging.AddZLoggerConsole();

       
// Add File Logging.
       logging.AddZLoggerFile(
"fileName.log");

       
// Add Rolling File Logging.
       logging.AddZLoggerRollingFile((dt, x) =>
$"logs/{dt.ToLocalTime():yyyy-MM-dd}_{x:000}.log", x => x.ToLocalTime().Date, 1024);

       
// Enable Structured Logging
       logging.AddZLoggerConsole(options =>
       {
           options.EnableStructuredLogging =
true;
       });
   })

public class MyClass
{
   
readonly ILogger<MyClass> logger;

   
// get logger from DI.
   
public class MyClass(ILogger<MyClass> logger)
   {
       
this.logger = logger;
   }

   
public void Foo()
   {
       
// log text.
       logger.ZLogDebug(
"foo{0} bar{1}", 10, 20);

       
// log text with structure in Structured Logging.
       logger.ZLogDebugWithPayload(
new { Foo = 10, Bar = 20 }, "foo{0} bar{1}", 10, 20);
   }
}

기본적으로 로거는 DI에 의해 주입된다(LogManager.GetLogger과 같은 방법을 원한다면 ReadMe # Global LoggerFactory 참조). LogDebug과 LogInformation 대신 앞에 Z를 붙인 ZLogDebug, ZLogInformation를 사용하는 것이 기본적인 흐름이다.

표준 Log 메서드(string format, object [] args)라는 메소드 정의를 위해 박싱은 피할 수 없다. 따라서 독자적인 제네릭에서 준비한 대량의 오버로드를 정의하고 있다.

// ZLog, ZLogTrace, ZLogDebug, ZLogInformation, ZLogWarning, ZLogError, ZLogCritical and *WithPayload.
public static void ZLogDebug(this ILogger logger, string format)
public static void ZLogDebug(this ILogger logger, EventId eventId, string format)
public static void ZLogDebug(this ILogger logger, Exception? exception, string format)
public static void ZLogDebug(this ILogger logger, EventId eventId, Exception? exception, string format)
public static void ZLogDebug<T1>(this ILogger logger, string format, T1 arg1)
public static void ZLogDebug<T1>(this ILogger logger, EventId eventId, string format, T1 arg1)
public static void ZLogDebug<T1>(this ILogger logger, Exception? exception, string format, T1 arg1)
public static void ZLogDebug<T1>(this ILogger logger, EventId eventId, Exception? exception, string format, T1 arg1)
// T1~T16
public static void ZLogDebug<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(this ILogger logger, EventId eventId, Exception? exception, string format, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16)

표준 Log 메서드와 ZLog 메소드가 혼합함으로써 실수 해 버리는(ZLog 메소드를 사용하려고 했는데 실수로 Log 메소드를 사용해 버리는) 가능성이 매우 높지만, 이것은 Microsoft.CodeAnalysis.BannedApiAnalyzers 를 사용하여 코드에서 표준 Log 메서드를 경고 / 오류 처리 할 수 있다.

이 BannedApiAnalyzers을 설정함으로써 확실히 ZLogger를 높은 성능으로 사용할 수 있다.

구조화 로깅

로그 관리 클라우드 서비스, 예를 들면 Datadog Logs와 Stackdriver Logging 등은 유연한 필터링 및 검색 식을 사용한 쿼리를 수행 할 수 있지만 이 기능을 최대한 살리기 위해서는 로그가 제대로 parse 되어 있어야 한다. 구조화 로깅(Structured Logging, Semantic Logging) 로그를 JSON 형식으로 보내는 방법으로 특별한 처리를 하지 않고 로그 서비스에 로드 시킬 수 있다. ZLogger는 EnableStructuredLogging를 사용하여 텍스트 메시지의 출력과 JSON 출력을 전환 할 수 있다. 또한 여러 로그 프로바이더에서 각각 전환 할 수 있도록 콘솔 출력은 문자 메시지, 파일 출력은 JSON 같은 설정도 가능하다.

logging.AddZLoggerConsole(options =>
{
   options.EnableStructuredLogging =
true;
});

// In default, output JSON with log information(categoryName, level, timestamp, exception), message and payload(if exists).

// {"CategoryName":"ConsoleApp.Program","LogLevel":"Information","EventId":0,"EventIdName":null,"Timestamp":"2020-04-07T11:53:22.3867872+00:00","Exception":null,"Message":"Registered User: Id = 10, UserName = Mike","Payload":null}
logger.ZLogInformation(
"Registered User: Id = {0}, UserName = {1}", id, userName);

// {"CategoryName":"ConsoleApp.Program","LogLevel":"Information","EventId":0,"EventIdName":null,"Timestamp":"2020-04-07T11:53:22.3867872+00:00","Exception":null,"Message":"Registered User: Id = 10, UserName = Mike","Payload":{"Id":10,"Name":"Mike"}}
logger.ZLogInformationWithPayload(
new UserRegisteredLog { Id = id, Name = userName }, "Registered User: Id = {0}, UserName = {1}", id, userName);

구조화 로깅에서도 System.Text.Json의 Utf8JsonWriter에 의한 UTF8에 직접 쓰기 처리나 또는 IBufferWriter을 활용한 투명 버퍼를 전달하여 JSON 처리에 관한 일체의 할당과 불필요한 복사의 발생에 의한 성능 저하를 철저히 막고 있다.

Microsoft.Extensions.Logging에 직접 구현

현재 대부분의 .NET Core 응용 프로그램은 .NET Generic Host 위에 구축되어 있기 때문에 (콘솔 응용 프로그램 조차도! 예를 들면 Cy#에서 개발하고 있는 ConsoleAppFramework는 쉽게 CLI 응용 프로그램을 .NET Generic Host에서 움직일 수 있다) Microsoft.Extensions에 의한 구성 로드, DI, 로거가 표준으로 동작하고 있다.

많은 로거는 별도의 프레임 워크를 가지고 Microsoft.Extensions.Logging으로 브리지 형태로 통합을 실현하고 있지만, 로그 출력 시 2개의 프레임 워크를 통과하는 파이프 라인이 길어지고 성능 저하로 연결된다.

Microsoft.Extensions.Logging은 로그 수준, 필터링, 여러 대상의 등록 등 충분히 프레임 워크로서의 기능을 가지고 있다. 따라서 자신의 로깅 프레임 워크를 만들지 않고 가능한한 직접 Microsoft.Extensions.Logging에 구현함으로써 오버 헤드를 최대한 줄이고 있다.

또한 Microsoft.Extensions.Logging의 출력 공급자는 현상 최소화 또는 Azure 의존 밖에 제공되지 않기 때문에 그냥으로 실용적으로 하기 어려운 점이 있었다. 이것이 다른 로깅 프레임 워크를 필요로 했던 이유이지만, ZLogger는 성능면 이외에, Console, File, RollingFile, Stream 등 충분한 제공자를 준비하여 Microsoft.Extensions.Logging만으로도 실용 수준으로 끌어 올렸다.

또한 초기화/종료 등이 모두 .NET Generic Host에 통합 되어 있기 때문에 로거 설정도 매우 간단하게 끝난다.

async Task Main()
{
   
await Host.CreateDefaultBuilder()
       .ConfigureLogging(logging =>
       {
           logging.ClearProviders();
           logging.AddZLoggerConsole();
       })
       .RunAsync();
}

Unity

Unity에서는 Debug.Log(내용은 Debug.unityLogger -> DebugLogHandler -> Internal_Log) 밖에 없는 이유가 아니라, 일단 ILogger, ILogHandler라는 추상 계층이 준비되어 있지만, 매우 빈약하고, 구현이 DebugLogHandler에만 존재하지 않는 한편, Debug.unityLogger가 대체 할 수 없어서 대부분 로깅 프레임워크로 작동하지 않을 것이다.

ZLogger을 Unity에서 사용하면 표준에 비해 "파일 출력을 포함한 여러 로그 프로바이더”  “표준 로그 수준 및 필터링" "로거 마다 카테고리 화” 등이 장점이다. 파일 출력은 모바일 응용 프로그램에서는 별로 쓸모가 없지만, VR 등 PC 응용 프로그램에는 필요하다.

또한 로거에 의한 분류 부여 등은 EditorConsolePro 등의 표준 보다 강력한 로그 콘솔에서 필터링에 매우 유용하다 (예 : [UI] [Battle, Network 등의 분류 부여).

정리

Cy# 에서는 현재 개발중인 응용 프로그램에서 .NET Core / MagicOnion 에서 구현된 컨테이너에서 호스팅 되는 실시간 서버 로그를 Datadog Logs 표준 출력을 통해 구조적 로그로 보내고 있다.

로깅은 옛날부터 성능 상의 현상이 발생하기 쉽고, 또한 지금도 유량이 많음으로 취급에 관해서는 신중하게 고려하여야 하는 영역이었다. ZLogger는 불필요한 설정도 없고, 표준 상태에서 최대의 성능을 발휘 하도록 설계되어 있다. 현대적인 관점에서 재 설계된 새로운 로깅 라이브러리를 꼭 시도해 보기 바란다.