LogTape는 카테고리 시스템으로 로그를 구조화합니다. Sonamu는 Model, Frame, Workflow, Agent 클래스에 대해 자동으로 카테고리를 생성합니다.
카테고리란?
카테고리는 로그의 출처를 계층 구조로 표현합니다.
형식: ["a", "b", "c"] 배열
예시:
["fastify"] - Fastify 프레임워크
["sonamu", "model", "user-model"] - UserModel 클래스
["sonamu", "workflow", "email-send"] - EmailSendWorkflow
["app", "payment", "processor"] - 커스텀 카테고리
카테고리 시스템의 목적
카테고리는 로그를 체계적으로 관리하기 위해 필요합니다.
1. 로그 출처 식별
// 어디서 발생한 로그인가?
["fastify"] // HTTP 요청
["sonamu", "model"] // Model 레이어
["app", "payment"] // 결제 로직
2. 선택적 로깅
// 특정 부분만 로깅
loggers: [
{
category: ["sonamu", "model"], // Model만 debug 레벨로
lowestLevel: "debug",
},
{
category: ["fastify"], // Fastify는 info 레벨로
lowestLevel: "info",
},
]
3. 로그 분리 저장
// 부분별로 다른 파일에
loggers: [
{
category: ["sonamu", "model"],
sinks: ["modelLog"], // models.log
},
{
category: ["app", "payment"],
sinks: ["paymentLog"], // payment.log
},
]
계층 구조의 이점
카테고리가 계층적인 이유:
1. 명확한 구조
["sonamu", "model", "user-model"]
// ↓ ↓ ↓
// 프레임워크 타입 구체적 이름
어디서 발생한 로그인지 한눈에 파악 가능
2. 유연한 필터링
// 전체 Sonamu 로그
category: ["sonamu"]
// Model 레이어만
category: ["sonamu", "model"]
// UserModel만
category: ["sonamu", "model", "user-model"]
3. 충돌 방지
// 다른 프로젝트의 "user" 로그도 구분 가능
["sonamu", "model", "user-model"] // Sonamu의 user
["app", "user", "service"] // 애플리케이션의 user
["external", "api", "user"] // 외부 API의 user
왜 정확한 매칭만 되나?
LogTape는 카테곣리를 정확히 일치하는 것만 매칭합니다.
// ❌ 부분 매칭 안 됨
category: ["sonamu"]
// → ["sonamu", "model", "user-model"] 로그는 매칭 안 됨
// ✅ 정확히 일치해야 함
category: ["sonamu", "model", "user-model"]
// → ["sonamu", "model", "user-model"] 로그만 매칭
이유:
- 명확성
// 부분 매칭이 된다면?
category: ["sonamu"]
// → ["sonamu", "model", "user-model"]
// → ["sonamu", "workflow", "email-send"]
// → ["sonamu", "agent", "payment-agent"]
// → 너무 많은 로그가 매칭됨!
// 정확한 매칭
category: ["sonamu", "model", "user-model"]
// → 원하는 로그만 정확히 매칭
- 성능
// 와일드카드 매칭이 된다면?
category: ["sonamu", "*"] // 모든 sonamu 하위 카테고리
// → 매번 패턴 매칭 필요 (느림)
// 정확한 매칭
category: ["sonamu", "model"]
// → 단순 문자열 비교 (빠름)
- 예측 가능성
// 부분 매칭이라면 혼란
loggers: [
{ category: ["sonamu"], lowestLevel: "info" },
{ category: ["sonamu", "model"], lowestLevel: "debug" },
]
// → ["sonamu", "model", "user-model"] 로그는 어디에 매칭?
// 정확한 매칭으로 명확
loggers: [
{ category: ["sonamu", "model"], lowestLevel: "debug" },
]
// → ["sonamu", "model"]만 매칭, 명확함!
카테고리 구조 상세
계층 표현
카테고리는 점점 구체적으로 좁혀집니다.
["sonamu", "model", "user-model"]
// ↓ ↓ ↓
// 네임스페이스 타입 구체적 이름
설정 예시:
export default defineConfig({
logging: {
loggers: [
// "sonamu"로 시작하는 모든 로그
{
category: ["sonamu"],
sinks: ["console"],
lowestLevel: "info",
},
// Model만
{
category: ["sonamu", "model"],
sinks: ["modelLog"],
lowestLevel: "debug",
},
// UserModel만
{
category: ["sonamu", "model", "user-model"],
sinks: ["userLog"],
lowestLevel: "debug",
},
],
},
server: {
// ...
},
});
카테고리 매칭
LogTape는 카테고리를 정확히 일치하는 것만 매칭합니다.
// ❌ 부분 매칭 안 됨
category: ["sonamu"]
// → ["sonamu", "model", "user-model"] 로그는 매칭 안 됨
// ✅ 정확히 일치해야 함
category: ["sonamu", "model", "user-model"]
// → ["sonamu", "model", "user-model"] 로그만 매칭
자동 카테고리 생성
Sonamu는 특정 클래스에 대해 자동으로 카테고리를 생성합니다.
Model 클래스
// UserModelClass → ["sonamu", "model", "user-model"]
// PostModelClass → ["sonamu", "model", "post-model"]
// OrderItemModelClass → ["sonamu", "model", "order-item-model"]
변환 규칙:
- 클래스 이름에서
ModelClass 제거
- PascalCase → snake_case 변환
- snake_case → kebab-case 변환
예시:
UserModelClass
→ User (ModelClass 제거)
→ user (snake_case)
→ ["sonamu", "model", "user"] (kebab-case, 접두사 추가)
OrderItemModelClass
→ OrderItem
→ order_item
→ ["sonamu", "model", "order-item"]
Frame 클래스
// UserFrameClass → ["sonamu", "frame", "user-frame"]
// AdminUserFrameClass → ["sonamu", "frame", "admin-user-frame"]
변환 규칙: Model과 동일하되 FrameClass 제거
Workflow
// EmailSendWorkflow → ["sonamu", "workflow", "email-send"]
// OrderProcessWorkflow → ["sonamu", "workflow", "order-process"]
변환 규칙:
- 클래스 이름에서
Workflow 제거 (접미사만)
- PascalCase → snake_case 변환
- snake_case → kebab-case 변환
예시:
EmailSendWorkflow
→ EmailSend (Workflow 제거)
→ email_send (snake_case)
→ ["sonamu", "workflow", "email-send"] (kebab-case, 접두사 추가)
Agent 클래스
// PaymentAgentClass → ["sonamu", "agent", "payment-agent"]
// AdminNotificationAgentClass → ["sonamu", "agent", "admin-notification-agent"]
변환 규칙: Model과 동일하되 AgentClass 제거
Naite 카테고리
Naite 테스트 키는 특별한 카테고리 변환 규칙을 따릅니다.
// "user.findOne" → ["user", "findOne"]
// "user:list" → ["user", "list"]
// "admin.company:detail" → ["admin", "company", "detail"]
변환 규칙:
.로 split
- 각 부분을
:로 split
- 평탄화 (flatten)
예시:
"user.findOne"
→ ["user", "findOne"]
"admin.company:detail"
→ split by "." → ["admin", "company:detail"]
→ split by ":" → [["admin"], ["company", "detail"]]
→ flatten → ["admin", "company", "detail"]
카테고리별 로깅 설정
Model별 로그 분리
import { defineConfig } from "sonamu";
import { getFileSink } from "@logtape/logtape";
export default defineConfig({
logging: {
sinks: {
userLog: getFileSink("logs/user-model.log"),
postLog: getFileSink("logs/post-model.log"),
},
loggers: [
{
category: ["sonamu", "model", "user-model"],
sinks: ["userLog"],
lowestLevel: "debug",
},
{
category: ["sonamu", "model", "post-model"],
sinks: ["postLog"],
lowestLevel: "debug",
},
],
},
server: {
// ...
},
});
타입별 로그 레벨 조정
export default defineConfig({
logging: {
loggers: [
// Model: debug 레벨
{
category: ["sonamu", "model"],
sinks: ["console"],
lowestLevel: "debug",
},
// Workflow: info 레벨
{
category: ["sonamu", "workflow"],
sinks: ["console"],
lowestLevel: "info",
},
// Agent: warning 레벨
{
category: ["sonamu", "agent"],
sinks: ["console"],
lowestLevel: "warning",
},
],
},
server: {
// ...
},
});
네임스페이스별 관리
export default defineConfig({
logging: {
sinks: {
sonamuLog: getFileSink("logs/sonamu.log"),
appLog: getFileSink("logs/app.log"),
},
loggers: [
// Sonamu 내부
{
category: ["sonamu"],
sinks: ["sonamuLog"],
lowestLevel: "info",
},
// 애플리케이션
{
category: ["app"],
sinks: ["appLog"],
lowestLevel: "debug",
},
],
},
server: {
// ...
},
});
커스텀 카테고리
애플리케이션 코드에서 커스텀 카테고리를 사용할 수 있습니다.
import { getLogger } from "@logtape/logtape";
// 로거 생성
const logger = getLogger(["app", "payment", "processor"]);
// 로깅
logger.info("Processing payment", { orderId: 123 });
logger.error("Payment failed", { error: "timeout" });
설정:
export default defineConfig({
logging: {
sinks: {
paymentLog: getFileSink("logs/payment.log"),
},
loggers: [
{
category: ["app", "payment", "processor"],
sinks: ["paymentLog"],
lowestLevel: "info",
},
],
},
server: {
// ...
},
});
실전 예시
개발 환경 상세 로깅
import { defineConfig } from "sonamu";
import { getConsoleSink } from "@logtape/logtape";
export default defineConfig({
logging: {
sinks: {
console: getConsoleSink(),
},
loggers: [
// Fastify: info
{
category: ["fastify"],
sinks: ["console"],
lowestLevel: "info",
},
// 모든 Sonamu 내부: debug
{
category: ["sonamu"],
sinks: ["console"],
lowestLevel: "debug",
},
// 앱 로직: debug
{
category: ["app"],
sinks: ["console"],
lowestLevel: "debug",
},
],
},
server: {
listen: { port: 1028 },
},
});
프로덕션 선택적 로깅
import { defineConfig } from "sonamu";
import { getConsoleSink, getFileSink } from "@logtape/logtape";
export default defineConfig({
logging: {
sinks: {
console: getConsoleSink(),
workflowLog: getFileSink("logs/workflow.log"),
errorLog: getFileSink("logs/error.log"),
},
loggers: [
// Fastify: warning 이상
{
category: ["fastify"],
sinks: ["console"],
lowestLevel: "warning",
},
// Workflow: 전체 기록
{
category: ["sonamu", "workflow"],
sinks: ["workflowLog"],
lowestLevel: "info",
},
// 에러만 별도 파일
{
category: ["sonamu"],
sinks: ["errorLog"],
lowestLevel: "error",
},
],
},
server: {
listen: { port: 1028 },
},
});
도메인별 로그 분리
import { defineConfig } from "sonamu";
import { getFileSink } from "@logtape/logtape";
export default defineConfig({
logging: {
sinks: {
userDomain: getFileSink("logs/user-domain.log"),
orderDomain: getFileSink("logs/order-domain.log"),
paymentDomain: getFileSink("logs/payment-domain.log"),
},
loggers: [
// User 관련
{
category: ["sonamu", "model", "user-model"],
sinks: ["userDomain"],
lowestLevel: "debug",
},
{
category: ["app", "user"],
sinks: ["userDomain"],
lowestLevel: "debug",
},
// Order 관련
{
category: ["sonamu", "model", "order-model"],
sinks: ["orderDomain"],
lowestLevel: "debug",
},
{
category: ["app", "order"],
sinks: ["orderDomain"],
lowestLevel: "debug",
},
// Payment 관련
{
category: ["app", "payment"],
sinks: ["paymentDomain"],
lowestLevel: "debug",
},
],
},
server: {
listen: { port: 1028 },
},
});
내부 동작 원리
이 섹션은 Sonamu의 카테고리 시스템이 내부적으로 어떻게 동작하는지 설명합니다. 여기 나오는 함수들은 Sonamu가 자동으로 사용하며, 사용자가 직접 호출할 수 없습니다.
isSameCategory()
두 카테고리가 정확히 일치하는지 확인하는 내부 함수입니다.
// Sonamu 내부 함수 (직접 사용 불가)
isSameCategory(["sonamu", "model"], ["sonamu", "model"]);
// → true
isSameCategory(["sonamu", "model"], ["sonamu", "workflow"]);
// → false
isSameCategory(["sonamu"], ["sonamu", "model"]);
// → false (길이가 다름)
내부 용도: LogTape의 logger 설정에서 카테고리를 매칭할 때 사용됩니다.
convertDomainToCategory()
클래스 이름을 카테고리로 변환하는 내부 함수입니다.
// Sonamu 내부 함수 (직접 사용 불가)
convertDomainToCategory("UserModelClass", "model");
// → ["sonamu", "model", "user-model"]
convertDomainToCategory("EmailSendWorkflow", "workflow");
// → ["sonamu", "workflow", "email-send"]
convertDomainToCategory("PaymentAgentClass", "agent");
// → ["sonamu", "agent", "payment-agent"]
내부 용도: Model, Frame, Workflow, Agent 클래스가 생성될 때 자동으로 카테고리를 할당합니다.
변환 로직:
- 클래스 이름에서 접미사 제거 (
ModelClass, FrameClass, AgentClass, Workflow)
- PascalCase → snake_case 변환
- snake_case → kebab-case 변환 (
_ → -)
["sonamu", type, name] 형식으로 구성
convertNaiteKeyToCategory()
Naite 테스트 키를 카테고리로 변환하는 내부 함수입니다.
// Sonamu 내부 함수 (직접 사용 불가)
convertNaiteKeyToCategory("user.findOne");
// → ["user", "findOne"]
convertNaiteKeyToCategory("admin.company:detail");
// → ["admin", "company", "detail"]
내부 용도: Naite 테스트 실행 시 로그 카테고리를 자동으로 생성합니다.
변환 로직:
.로 split
- 각 부분을
:로 split
- 평탄화 (flatten)
주의사항
1. 정확한 매칭만 지원
// ❌ 와일드카드 없음
category: ["sonamu", "*"] // 작동 안 함
// ❌ 부분 매칭 없음
category: ["sonamu"] // ["sonamu", "model"]과 매칭 안 됨
// ✅ 정확한 카테고리만
category: ["sonamu", "model", "user-model"]
2. 카테고리는 readonly 배열
// ✅ readonly 배열 사용
const category: readonly string[] = ["app", "payment"];
// ❌ 일반 배열은 타입 에러 가능
const category: string[] = ["app", "payment"];
3. 클래스 이름 컨벤션
// ✅ 올바른 이름
class UserModelClass { } // → ["sonamu", "model", "user-model"]
class OrderItemModelClass { } // → ["sonamu", "model", "order-item-model"]
class EmailSendWorkflow { } // → ["sonamu", "workflow", "email-send"]
// ❌ 컨벤션 위반 (자동 변환 안 됨)
class User { } // Model이 아님
class UserModel { } // "Class" 접미사 없음
class WorkflowEmailSend { } // "Workflow" 접두사 (접미사여야 함)
4. Fastify 카테고리 중복
// ❌ Fastify 카테고리와 충돌
fastifyCategory: ["sonamu", "model"]
loggers: [
{
category: ["sonamu", "model"], // Fastify 로그와 섞임!
// ...
},
]
// ✅ 분리된 카테고리
fastifyCategory: ["fastify"]
loggers: [
{
category: ["sonamu", "model"],
// ...
},
]
다음 단계