메인 콘텐츠로 건너뛰기
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"] 로그만 매칭
이유:
  1. 명확성
// 부분 매칭이 된다면?
category: ["sonamu"]
// → ["sonamu", "model", "user-model"]
// → ["sonamu", "workflow", "email-send"]
// → ["sonamu", "agent", "payment-agent"]
// → 너무 많은 로그가 매칭됨!

// 정확한 매칭
category: ["sonamu", "model", "user-model"]
// → 원하는 로그만 정확히 매칭
  1. 성능
// 와일드카드 매칭이 된다면?
category: ["sonamu", "*"]  // 모든 sonamu 하위 카테고리
// → 매번 패턴 매칭 필요 (느림)

// 정확한 매칭
category: ["sonamu", "model"]
// → 단순 문자열 비교 (빠름)
  1. 예측 가능성
// 부분 매칭이라면 혼란
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"]
변환 규칙:
  1. 클래스 이름에서 ModelClass 제거
  2. PascalCase → snake_case 변환
  3. 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"]
변환 규칙:
  1. 클래스 이름에서 Workflow 제거 (접미사만)
  2. PascalCase → snake_case 변환
  3. 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"]
변환 규칙:
  1. .로 split
  2. 각 부분을 :로 split
  3. 평탄화 (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 클래스가 생성될 때 자동으로 카테고리를 할당합니다. 변환 로직:
  1. 클래스 이름에서 접미사 제거 (ModelClass, FrameClass, AgentClass, Workflow)
  2. PascalCase → snake_case 변환
  3. snake_case → kebab-case 변환 (_-)
  4. ["sonamu", type, name] 형식으로 구성

convertNaiteKeyToCategory()

Naite 테스트 키를 카테고리로 변환하는 내부 함수입니다.
// Sonamu 내부 함수 (직접 사용 불가)
convertNaiteKeyToCategory("user.findOne");
// → ["user", "findOne"]

convertNaiteKeyToCategory("admin.company:detail");
// → ["admin", "company", "detail"]
내부 용도: Naite 테스트 실행 시 로그 카테고리를 자동으로 생성합니다. 변환 로직:
  1. .로 split
  2. 각 부분을 :로 split
  3. 평탄화 (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"],
    // ...
  },
]

다음 단계