예전부터 CI 시스템에서 대량의 로그 파일 작업을 하는 것은 어려운 일이었습니다. 이전에 여러 빌드 인프라를 구현해 오면서 일반 텍스트 로그, 대량의 쿠킹 로그를 열 때 발생하는 브라우저 크래시, 빈약한 검색 성능 등에 익숙해졌습니다.
로그는 처음부터 호드에서 제대로 만들고자 했던 요소들 중 하나로, 가장 우선적으로 필요했던 기능은 다음과 같습니다.
- 대량의 로그를 통해 인덱싱된 텍스트 검색 빠르게 수행하기
- 로그를 클라이언트에 완전히 다운로드하지 않고 로그 파일 스크럽하기
- 빌드 상태 문제에 대한 크로스 레퍼런스, Perforce 히스토리, 오류 코드의 의미를 설명하는 외부 사이트 등 로그 내 이벤트에 훨씬 더 풍부한 컨텍스트 기반 기능 제공하기
스토리지
호드 로그는 여러 노드 타입을 사용하여 번들에 저장됩니다. 이러한 모든 클래스는 Engine/Source/Programs/Shared/EpicGames.Horde/Logs 에 구현됩니다.
LogNode오브젝트는 로그 데이터의 메인 엔트리 포인트로, 로그에 대한 메타데이터(줄 개수, 길이, 계속 추가되고 있는지 여부)가 전체적으로 포함되어 있을 뿐만 아니라 로그 데이터가 있는 청크 및 인덱스 노드에 대한 레퍼런스도 포함되어 있습니다.LogChunkNode오브젝트에는 연속된 여러 줄에 대한 원시 UTF-8 인코딩 구조화 로그 데이터와 그 안의 줄을 빠르게 인덱싱하기 위한 오프셋이 포함되어 있습니다. 루트LogNode오브젝트의 청크에 대한 각 레퍼런스에는 시작 줄 번호가 포함되어 있어 어떤 노드에 특정 줄이 포함되어 있는지 빠르게 식별할 수 있습니다.LogIndexNode오브젝트에는 어떤 청크에 특정 검색어와 청크의 일반 텍스트 렌더링이 들어 있는지 결정하는 데 사용되는 데이터 청크가 포함되어 있습니다. 인덱스/검색 알고리즘은 아래 섹션에 설명되어 있습니다.
로그를 생성하는 에이전트는 보통 스토리지 백엔드에 바로 데이터를 업로드하며, 새 루트 노드와 추가된 청크 및 인덱스 노드를 업로드하여 데이터를 추가합니다.
인덱싱
인덱스는 UTF-8 로그 메시지의 일반 텍스트 렌더링을 통해 생성됩니다. ANSI 문자 A-Z 는 소문자 형식인 a-z 로 변환됩니다.
먼저 메시지가 토큰으로 분할되며, 각 토큰에는 다음 타입의 문자만 포함됩니다.
- 0: 아래 어떤 카테고리에도 속하지 않음
- 1: 알파벳 문자
[a-zA-Z] - 2: 숫자
[0-9] - 3: 공백 문자
[ \t] - 4: 줄바꿈 문자
\n
예를 들어 hello world123 은 hello, ` ` , world , 123 이라는 4개의 토큰으로 분할됩니다.
그런 다음 각 토큰은 32비트 ngram으로 추가 분해됩니다. 즉 연속된 4개의 문자 시퀀스가 포함된 32비트 인티저 값으로 분해되며, 부분 ngram은 더 중요한 비트 쪽인 왼쪽으로 이동합니다.
"hell" = ('h' << 24) | ('e' << 16) | ('l' << 8) | 'l' = 0x68656c6c
"o" = ('o' << 24) = 0x6f000000
" " = (' ' << 24) = 0x20000000
"worl" = ('w' << 24) | ('o' << 16) | ('r' << 8) | 'l' = 0x776f726c
"d" = ('d' << 24) = 0x64000000
"123" = ('1' << 24) | ('2' << 16) | ('3' << 8) = 0x31323300
이러한 값은 64비트 인티저 공간의 희소 트라이(NgramSet)에 삽입되며, ngram 값은 상위 32비트에, ngram이 포함된 블록의 인덱스는 하위 32비트에 들어갑니다. 이를 통해 로그 파일에서 ngram을 찾을 수 있는 위치를 대략적으로 검색하기 위한 매우 공간 효율적인 데이터 구조가 형성됩니다.
특정 스트링을 검색하는 경우 해당 스트링에서 ngram으로의 동일한 변환을 수행하여 검색어의 모든 ngram이 포함된 모든 블록 인덱스를 찾습니다. 검색어의 첫 번째 토큰과 마지막 토큰은 더 길거나 더 짧은 접두사를 보유한 소스 토큰 내에서 일치할 수 있어 두 토큰에 대한 특수 처리가 취해지므로 ngram으로 분해되기 전에 정렬이 변경됩니다.
검색어에 대한 잠재적 일치를 확보한 후에는 간소화된 KMP(Knuth–Morris–Pratt) 알고리즘을 통해 청크를 가져와서 정확한 용어를 검색하는, 보다 비용이 많이 드는 작업을 수행할 수 있습니다.
테일링
로그 테일링은 로그가 아직 완료로 표시되지 않은 경우 처음 요청될 때 활성화됩니다. 로그 테일링 요청은 Redis에 30초의 TTL로 저장되며, 값은 후속 호출에서 리셋됩니다.
로그를 업로드하는 에이전트는 테일링이 필요한지 여부를 확인하기 위해 서버를 폴링합니다. 활성화 이후 에이전트는 아직 서버에 직접 영구 스토리지로 플러시되지 않은 데이터를 업로드하기 시작하며, 이 경우 데이터는 Redis에 저장되고 로그 후속 읽기 시 클라이언트로 반환됩니다.
JSON 구문
호드 에이전트에서 생성된 로그는 한 줄당 하나의 JSON 오브젝트로 구성됩니다. 프로퍼티는 다음과 같습니다.
| 이름 | 타입 | 설명 |
|---|---|---|
time |
String |
이벤트가 발생한 타임스탬프입니다(UTC 기준). |
level |
String |
이벤트 레벨입니다. 유효한 값은 여기에 나열되어 있습니다. |
id |
Integer |
이벤트 식별자입니다. 알려진 이벤트 ID는 Engine/Source/Progams/Shared/EpicGames.Core/KnownLogEvents.cs. 에 나열되어 있습니다. |
message |
String |
렌더링된 이벤트 메시지입니다. |
format |
String |
표준 메시지 템플릿 구문을 사용하여 메시지를 렌더링하기 위한 포맷 스트링입니다. |
properties |
Object |
포맷 스트링의 프로퍼티에 대한 딕셔너리입니다. 자세한 내용은 아래에서 확인하세요. |
lineIndex |
Integer |
여러 줄의 메시지에서 메시지 내 이 로그 이벤트의 0 기반 인덱스를 나타냅니다. |
lineCount |
Integer |
여러 줄의 메시지에서 이 메시지의 총 줄 수를 나타냅니다. |
프로퍼티는 일반적인 JSON 타입일 수도 있고 $type 프로퍼티가 포함된 오브젝트일 수도 있습니다. 이는 대시보드가 프로퍼티를 특정한 타입으로 간주하여 그에 따라 렌더링할 수 있음을 나타냅니다. $type 의 유효한 값은 Engine/Source/Programs/Shared/EpicGames.Core/LogValue.cs 의 LogValueType 클래스에 정의되어 있으며, 다음이 포함됩니다.
| 이름 | 설명 | 추가 필드 |
|---|---|---|
Asset |
에셋에 대한 경로입니다. | file : 에셋이 포함된 로컬 파일입니다.depotPath : 에셋이 포함된 파일의 Perforce 디포 경로입니다. |
SourceFile |
소스 코드가 포함된 파일에 대한 경로입니다. | file : 에셋이 포함된 로컬 파일입니다.depotPath : 에셋이 포함된 파일의 Perforce 디포 경로입니다. |
Channel |
언리얼 엔진 채널 이름입니다. | |
Severity |
언리얼 엔진 채널 심각도입니다. | |
LineNumber |
컴파일러 오류 메시지의 줄 번호입니다. | |
ColumnNumber |
컴파일러 오류 메시지의 열 번호입니다. | |
Symbol |
C++ 링커 심볼입니다. | identifier : 꾸미지 않은 심볼 이름입니다. |
ToolName |
표준 Microsoft 오류에서 오류를 생성하는 툴의 이름을 나타냅니다. | |
ScreenshotTest |
실패한 스크린샷 테스트의 이름입니다. |
$type 필드가 지정된 경우 $text 필드도 존재할 수 있습니다. 이는 호드가 상단에 추가할 수 있는 탐색 또는 컨텍스트에 따른 기능과 관계없이 소스에서 결정된 필드의 의도된 렌더링을 나타냅니다.