내 라이브러리에 맞는 로깅 라이브러리를 찾지 못해서 직접 만들었습니다
洪 民憙 (Hong Minhee) @hongminhee@hackers.pub
Fedify, ActivityPub 서버 프레임워크를 개발하기 시작했을 때, 의외의 문제에 부딪혔습니다: 로깅을 추가하는 방법을 찾지 못했습니다.
로깅 자체가 어려워서가 아닙니다—JavaScript용 성숙한 로깅 라이브러리는 수십 개가 있습니다. 문제는 이들이 주로 애플리케이션을 위해 설계되었지, 방해가 되지 않기를 원하는 라이브러리를 위한 것이 아니라는 점이었습니다.
저는 몇 달 전에 이에 대해 글을 썼고, 반응은 소박했습니다—약간의 관심, 약간의 회의론, 그리고 이 글이 AI로 생성되었는지에 대한 꽤 많은 논쟁이 있었습니다. 솔직히 말하자면: 영어는 제 모국어가 아니기 때문에 글을 다듬기 위해 LLM을 사용합니다. 하지만 아이디어와 기술적 내용은 제 것입니다.
몇몇 독자들은 이론보다 실제 사례를 보고 싶어했습니다.
문제: 기존 로거들은 앱을 만든다고 가정합니다
Fedify는 개발자들이 ActivityPub 프로토콜을 사용하여 연합형 소셜 애플리케이션을 구축하는 데 도움을 줍니다. 연합(federation)과 작업해 본 적이 있다면, 디버깅이 얼마나 고통스러울 수 있는지 아실 겁니다. 활동(activity) 전달이 실패했을 때, 다음과 같은 질문에 답해야 합니다:
- HTTP 요청이 실제로 나갔나요?
- 서명이 올바르게 생성되었나요?
- 원격 서버가 이를 거부했나요? 왜 그랬나요?
- 응답 파싱에 문제가 있었나요?
이러한 질문들은 여러 하위 시스템에 걸쳐 있습니다: HTTP 처리, 암호화 서명, JSON-LD 처리, 큐 관리 등. 좋은 로깅 없이는 디버깅이 추측 게임이 됩니다.
하지만 라이브러리 작성자로서 제가 직면한 딜레마는 이것이었습니다: 디버깅을 돕기 위해 상세한 로깅을 추가하면, 콘솔이 Fedify의 내부 잡담으로 어지러워지는 것을 원치 않는 사용자들을 짜증나게 할 위험이 있습니다. 침묵을 지키면, 사용자들은 문제를 진단하는 데 어려움을 겪습니다.
기존 옵션들을 살펴봤습니다. winston이나 Pino를 사용하면 다음 중 하나를 해야 했습니다:
- Fedify 내부에 로거를 구성하거나(사용자에게 내 선택을 강요), 또는
- 사용자에게 로거 인스턴스를 Fedify에 전달하도록 요청(상용구 코드 추가)
또한 debug도 있는데, 이는 이런 사용 사례를 위해 설계되었습니다. 하지만 운영 팀이 기대하는 구조화된, 레벨 기반 로그를 제공하지 않습니다—그리고 환경 변수에 의존하는데, Deno와 같은 일부 런타임은 보안상의 이유로 기본적으로 이를 제한합니다.
이 중 어느 것도 적합하지 않았습니다. 그래서 저는 LogTape—라이브러리 작성자를 위해 처음부터 설계된 로깅 라이브러리를 만들었습니다. 그리고 Fedify는 그 첫 번째 실제 사용자가 되었습니다.
해결책: 기본 출력이 없는 계층적 카테고리
핵심 통찰은 간단했습니다: 라이브러리는 애플리케이션 개발자가 명시적으로 활성화하지 않는 한 어떤 출력도 생성하지 않고 로깅할 수 있어야 합니다.
Fedify는 LogTape의 계층적 카테고리 시스템을 사용하여 사용자에게 보고 싶은 것에 대한 세밀한 제어 권한을 제공합니다. 카테고리는 다음과 같이 구성되어 있습니다:
| 카테고리 | 로깅 내용 |
|---|---|
["fedify"] |
라이브러리의 모든 것 |
["fedify", "federation", "inbox"] |
수신 활동 |
["fedify", "federation", "outbox"] |
발신 활동 |
["fedify", "federation", "http"] |
HTTP 요청 및 응답 |
["fedify", "sig", "http"] |
HTTP 서명 작업 |
["fedify", "sig", "ld"] |
링크드 데이터 서명 작업 |
["fedify", "sig", "key"] |
키 생성 및 검색 |
["fedify", "runtime", "docloader"] |
JSON-LD 문서 로딩 |
["fedify", "webfinger", "lookup"] |
WebFinger 리소스 조회 |
...그리고 약 십여 개 더 있습니다. 각 카테고리는 별개의 하위 시스템에 해당합니다.
이는 사용자가 다음과 같이 로깅을 구성할 수 있음을 의미합니다:
await configure({
sinks: { console: getConsoleSink() },
loggers: [
// Fedify의 모든 오류 표시
{ category: "fedify", sinks: ["console"], lowestLevel: "error" },
// 하지만 특별히 inbox 처리에 대한 디버그 정보 표시
{ category: ["fedify", "federation", "inbox"], sinks: ["console"], lowestLevel: "debug" },
],
});
수신 활동에 문제가 생기면, 다른 모든 것은 조용히 유지하면서 해당 하위 시스템에 대한 상세한 로그를 얻을 수 있습니다. 코드 변경이 필요 없습니다—단지 구성만 필요합니다.
암시적 컨텍스트를 통한 요청 추적
계층적 카테고리는 필터링 문제를 해결했지만, 또 다른 과제가 있었습니다: 비동기 경계를 넘어 로그를 연관시키는 것입니다.
연합 시스템에서는 단일 사용자 작업이 일련의 작업을 촉발할 수 있습니다: 원격 액터 가져오기, 서명 확인, 활동 처리, 팔로워에게 전파 등. 무언가 실패했을 때, 해당 특정 요청에 대한 모든 로그 항목을 연관시켜야 합니다.
Fedify는 LogTape의 암시적 컨텍스트 기능을 사용하여 모든 로그 항목에 자동으로 requestId를 태그합니다:
await configure({
sinks: {
file: getFileSink("fedify.jsonl", { formatter: jsonLinesFormatter })
},
loggers: [
{ category: "fedify", sinks: ["file"], lowestLevel: "info" },
],
contextLocalStorage: new AsyncLocalStorage(), // 암시적 컨텍스트 활성화
});
이 구성을 사용하면 모든 로그 항목에 자동으로 requestId 속성이 포함됩니다. 특정 요청을 디버깅해야 할 때 로그를 필터링할 수 있습니다:
jq 'select(.properties.requestId == "abc-123")' fedify.jsonl
그러면 해당 요청의 모든 로그 항목을 볼 수 있습니다—모든 하위 시스템에 걸쳐, 모두 순서대로. 수동 상관관계 분석이 필요 없습니다.
requestId는 가능한 경우 표준 헤더(X-Request-Id, Traceparent 등)에서 파생되므로, 기존 관찰성 인프라와 자연스럽게 통합됩니다.
사용자가 실제로 보는 것
그렇다면 이 모든 구성이 Fedify를 사용하는 사람에게 실제로 어떤 의미가 있을까요?
Fedify 사용자가 LogTape를 전혀 구성하지 않으면, 아무것도 보이지 않습니다. 누락된 구성에 대한 경고도 없고, 기본 출력도 없으며, 성능 오버헤드도 최소화됩니다—로깅 호출은 본질적으로 아무 작업도 하지 않습니다.
기본적인 가시성을 위해, 세 줄의 구성으로 Fedify의 모든 오류 수준 로깅을 활성화할 수 있습니다. 특정 문제를 디버깅할 때는 관련 하위 시스템에 대해서만 디버그 수준 로깅을 활성화할 수 있습니다.
그리고 심각한 관찰성 요구 사항이 있는 프로덕션 환경에서 실행 중이라면, 요청 상관관계가 내장된 구조화된 JSON 로그를 모니터링 시스템으로 전송할 수 있습니다.
동일한 라이브러리 코드가 이 모든 시나리오를 지원합니다—사용자가 Node.js, Deno, Bun 또는 엣지 함수에서 실행하든, 추가 폴리필이나 심(shim) 없이 가능합니다. 사용자가 필요한 것을 결정합니다.
배운 교훈
LogTape로 Fedify를 구축하면서 몇 가지를 배웠습니다:
카테고리를 일찍 설계하세요. 계층적 구조는 사용자가 실제로 로그를 필터링하고 싶어하는 방식을 반영해야 합니다. 저는 Fedify의 카테고리를 사용자가 독립적으로 디버깅해야 할 수 있는 하위 시스템을 중심으로 구성했습니다.
구조화된 로깅을 사용하세요. requestId, activityId, actorId와 같은 속성은 프로그래밍 방식으로 로그를 분석해야 할 때 문자열 보간보다 훨씬 더 유용합니다.
암시적 컨텍스트가 예상보다 더 유용한 것으로 판명되었습니다. 컨텍스트를 수동으로 전달하지 않고도 비동기 경계를 넘어 로그를 연관시킬 수 있어 분산 작업 디버깅이 훨씬 쉬워졌습니다. 사용자가 활동 전달이 실패했다고 보고할 때, 관련된 모든 것을 추출하는 단일 jq 명령을 제공할 수 있습니다.
사용자를 신뢰하세요. 일부 라이브러리 작성자는 로그를 통해 너무 많은 내부 세부 정보를 노출하는 것을 걱정합니다. 저는 반대의 경험을 했습니다—사용자들은 필요할 때 무슨 일이 일어나고 있는지 볼 수 있다는 것을 감사하게 생각합니다. 핵심은 옵트인(opt-in) 방식으로 만드는 것입니다.
직접 시도해 보세요
라이브러리를 구축하면서 로깅 문제—얼마나 많이 로깅할지, 사용자에게 어떻게 제어권을 줄지, 어떻게 시끄럽지 않게 할지—로 고민하고 계시다면, Fedify가 어떻게 하는지 살펴보시길 권장합니다.
Fedify 로깅 문서에서 모든 것을 자세히 설명합니다. 그리고 LogTape 설계 철학을 이해하고 싶다면, 제 이전 글에서 다루고 있습니다.
LogTape은 해당 도구에 만족하는 애플리케이션 개발자를 위해 winston이나 Pino를 대체하려는 것이 아닙니다. 이는 다른 간극을 메웁니다: 사용자가 필요로 할 때까지 방해가 되지 않기를 원하는 라이브러리를 위한 로깅입니다. 그것이 여러분이 찾고 있는 것이라면, 일반적인 앱 중심 로거보다 더 적합할 수 있습니다.