> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Sinks & Filters

> 로그 출력 대상과 필터링 설정

Sink는 로그를 출력할 대상을, Filter는 어떤 로그를 출력할지 결정합니다. `sonamu.config.ts`의 `logging` 옵션에서 커스텀 sink와 filter를 추가할 수 있습니다.

## Sink란?

Sink는 로그가 최종적으로 출력되는 곳입니다.

**예시:**

* 콘솔 (터미널)
* 파일
* 외부 로깅 서비스 (Sentry, Datadog 등)
* 데이터베이스

### Sink가 필요한 이유

로그는 목적에 따라 다른 곳에 기록되어야 합니다:

**1. 실시간 모니터링 (콘솔)**

```typescript theme={null}
// 개발 중 즉시 확인
logger.info("API called", { url: "/api/user" });
// → 터미널에 바로 출력
```

**2. 영구 보관 (파일)**

```typescript theme={null}
// 나중에 분석하기 위해
logger.error("Payment failed", { orderId });
// → errors.log 파일에 저장
```

**3. 에러 알림 (외부 서비스)**

```typescript theme={null}
// 심각한 문제 즉시 통보
logger.fatal("DB connection lost");
// → Sentry에 전송, 슬랙 알림
```

**4. 통계 분석 (데이터베이스)**

```typescript theme={null}
// 로그 데이터 분석
logger.info("User action", { action: "purchase", amount });
// → DB에 저장, 나중에 집계
```

### 여러 Sink 동시 사용

한 로그를 여러 Sink에 동시에 보낼 수 있습니다:

```typescript theme={null}
loggers: [
  {
    category: ["fastify"],
    sinks: ["console", "file", "sentry"], // 3곳에 동시 기록
    lowestLevel: "error",
  },
];
```

**장점:**

* 실시간 확인 (console)
* 영구 보관 (file)
* 즉시 알림 (sentry)

## Filter란?

Filter는 로그를 선택적으로 출력하기 위한 조건입니다.

**예시:**

* 특정 카테고리만
* 특정 레벨 이상만
* 특정 URL 패턴만
* 시간대별로

### Filter가 필요한 이유

모든 로그를 다 기록하면 문제가 발생합니다:

**1. 성능 저하**

```typescript theme={null}
// Filter 없음: 모든 요청 로깅
/favicon.ico  / /
  불필요 /
  assets /
  logo.png / // 불필요
  api /
  healthcheck / // 반복적, 불필요
  api /
  user /
  list / // 필요!
  // Filter 있음: /api만 로깅
  api /
  user /
  list; // 필요!
```

**2. 디스크 공간 낭비**

```typescript theme={null}
// 하루 100만 건의 요청
// Filter 없음: 100만 건 모두 기록 → 수백 MB
// Filter 있음: 10만 건만 기록 → 수십 MB
```

**3. 가독성 저하**

```
[로그 파일]
[INFO] /favicon.ico
[INFO] /assets/style.css
[INFO] /assets/logo.png
[INFO] /api/healthcheck
[INFO] /api/healthcheck
[INFO] /api/user/list  // ← 필요한 로그를 찾기 어려움!
[INFO] /api/healthcheck
```

### Filter 사용 예시

```typescript theme={null}
filters: {
  // 중요한 것만
  "important": (record) => {
    // API 요청만
    if (!record.url.startsWith("/api")) return false;
    // healthcheck 제외
    if (record.url === "/api/healthcheck") return false;
    return true;
  },
}
```

## Sink vs Filter 차이점

두 개념은 서로 다른 목적을 가집니다:

| 구분      | Sink           | Filter         |
| ------- | -------------- | -------------- |
| **목적**  | 어디에 기록?        | 무엇을 기록?        |
| **타이밍** | 로그 출력 시        | 필터링 시          |
| **예시**  | 콘솔, 파일, Sentry | URL 패턴, 레벨, 시간 |
| **역할**  | 출력 형식 결정       | 출력 여부 결정       |

### 함께 사용하기

```typescript theme={null}
{
  sinks: {
    console: getConsoleSink(),      // 어디에?
    errorFile: getFileSink("..."),  // 어디에?
  },

  filters: {
    "api-only": (record) => ...,   // 무엇을?
    "errors": (record) => ...,      // 무엇을?
  },

  loggers: [
    {
      category: ["fastify"],
      sinks: ["console"],           // console에
      filters: ["api-only"],        // API 로그만
    },
    {
      category: ["fastify"],
      sinks: ["errorFile"],         // errorFile에
      filters: ["errors"],          // 에러 로그만
    },
  ],
}
```

## 언제 사용하나?

### Sink를 추가할 때

**1. 새로운 출력 대상이 필요할 때**

```typescript theme={null}
// 예: Slack으로 에러 알림
sinks: {
  slack: customSlackSink,
}
```

**2. 다른 형식으로 기록할 때**

```typescript theme={null}
// 예: JSON 형식으로 파일 저장
sinks: {
  jsonFile: getFileSink("app.json", { formatter: jsonFormatter }),
}
```

**3. 여러 곳에 동시 기록할 때**

```typescript theme={null}
loggers: [
  {
    sinks: ["console", "file", "sentry"], // 3곳에 동시
  },
];
```

### Filter를 추가할 때

**1. 특정 조건의 로그만 필요할 때**

```typescript theme={null}
// 예: 느린 요청만
filters: {
  "slow": (record) => record.responseTime > 1000,
}
```

**2. 성능을 개선하고 싶을 때**

```typescript theme={null}
// 예: 불필요한 로그 제외
filters: {
  "no-healthcheck": (record) => record.url !== "/api/healthcheck",
}
```

**3. Sink별로 다른 로그를 보내고 싶을 때**

```typescript theme={null}
loggers: [
  {
    sinks: ["console"],
    filters: ["all"], // 콘솔: 모든 로그
  },
  {
    sinks: ["errorFile"],
    filters: ["errors"], // 파일: 에러만
  },
];
```

## Sinks 설정 상세

### sinks 옵션

`logging.sinks`에 커스텀 Sink를 추가합니다.

**타입**: `Record<string, Sink>`

```typescript theme={null}
import { defineConfig } from "sonamu";
import { getConsoleSink, getFileSink } from "@logtape/logtape";

export default defineConfig({
  logging: {
    sinks: {
      "my-console": getConsoleSink(),
      "my-file": getFileSink("logs/app.log"),
    },
  },

  server: {
    // ...
  },
});
```

<Warning>
  `"fastify-console"` 이름은 Sonamu가 자동으로 생성하는 기본 Sink입니다. 이 이름으로 추가하면 기본
  Sink를 덮어씁니다.
</Warning>

### 콘솔 Sink

터미널에 로그를 출력합니다.

```typescript theme={null}
import { getConsoleSink } from "@logtape/logtape";
import { getPrettyFormatter } from "@logtape/pretty";

export default defineConfig({
  logging: {
    sinks: {
      console: getConsoleSink({
        formatter: getPrettyFormatter({
          timestamp: "time",
          categoryWidth: 20,
        }),
      }),
    },
  },

  server: {
    // ...
  },
});
```

### 파일 Sink

파일에 로그를 저장합니다.

```typescript theme={null}
import { getFileSink } from "@logtape/logtape";

export default defineConfig({
  logging: {
    sinks: {
      accessLog: getFileSink("logs/access.log"),
      errorLog: getFileSink("logs/errors.log"),
    },

    loggers: [
      // 모든 로그는 access.log에
      {
        category: ["fastify"],
        sinks: ["accessLog"],
        lowestLevel: "info",
      },
      // 에러만 errors.log에
      {
        category: ["fastify"],
        sinks: ["errorLog"],
        lowestLevel: "error",
      },
    ],
  },

  server: {
    // ...
  },
});
```

### 스트림 Sink

Node.js 스트림에 로그를 출력합니다.

```typescript theme={null}
import { getStreamSink } from "@logtape/logtape";
import fs from "fs";

const stream = fs.createWriteStream("logs/app.log", { flags: "a" });

export default defineConfig({
  logging: {
    sinks: {
      stream: getStreamSink(stream),
    },
  },

  server: {
    // ...
  },
});
```

### 커스텀 Sink

직접 Sink를 구현할 수 있습니다.

```typescript theme={null}
import type { Sink, LogRecord } from "@logtape/logtape";
import * as Sentry from "@sentry/node";

const sentrySink: Sink = async (record: LogRecord) => {
  if (record.level >= "error") {
    Sentry.captureException(new Error(record.message.join(" ")), {
      level: record.level,
      extra: record.properties,
    });
  }
};

export default defineConfig({
  logging: {
    sinks: {
      sentry: sentrySink,
    },

    loggers: [
      {
        category: ["fastify"],
        sinks: ["fastify-console", "sentry"], // 콘솔 + Sentry
        lowestLevel: "info",
      },
    ],
  },

  server: {
    // ...
  },
});
```

## Filters 설정

### filters 옵션

로그를 선택적으로 필터링합니다.

**타입**: `Record<string, FilterLike>`

```typescript theme={null}
type FilterLike = Filter | string;
type Filter = (record: LogRecord) => boolean;
```

```typescript theme={null}
import type { LogRecord } from "@logtape/logtape";

export default defineConfig({
  logging: {
    filters: {
      "api-only": (record: LogRecord) => {
        const req = record.properties.req;
        return req?.url?.startsWith("/api") ?? false;
      },
    },
  },

  server: {
    // ...
  },
});
```

<Warning>
  `"fastify-console"` 이름은 Sonamu가 자동으로 생성하는 기본 Filter입니다. 이 이름으로 추가하면 기본
  Filter를 덮어씁니다.
</Warning>

### URL 기반 필터

```typescript theme={null}
import type { LogRecord } from "@logtape/logtape";
import type { FastifyRequest, FastifyReply } from "fastify";

export default defineConfig({
  logging: {
    filters: {
      "user-api": (record: LogRecord) => {
        const req = record.properties.req as FastifyRequest | undefined;
        const res = record.properties.res as FastifyReply | undefined;
        const url = req?.url ?? res?.request.url;
        return url?.startsWith("/api/user") ?? false;
      },
    },

    loggers: [
      {
        category: ["fastify"],
        sinks: ["fastify-console"],
        filters: ["user-api"], // /api/user/* 만
        lowestLevel: "info",
      },
    ],
  },

  server: {
    // ...
  },
});
```

### 레벨 기반 필터

```typescript theme={null}
export default defineConfig({
  logging: {
    filters: {
      "error-only": (record) => record.level >= "error",
      "warning-or-higher": (record) => record.level >= "warning",
    },
  },

  server: {
    // ...
  },
});
```

### 시간 기반 필터

```typescript theme={null}
export default defineConfig({
  logging: {
    filters: {
      "business-hours": (record) => {
        const hour = new Date().getHours();
        return hour >= 9 && hour < 18; // 09:00 - 18:00만
      },
    },
  },

  server: {
    // ...
  },
});
```

### 프로퍼티 기반 필터

```typescript theme={null}
export default defineConfig({
  logging: {
    filters: {
      "slow-requests": (record) => {
        const responseTime = record.properties.responseTime as number | undefined;
        return responseTime ? responseTime > 1000 : false; // 1초 이상만
      },
    },
  },

  server: {
    // ...
  },
});
```

## 실전 예시

### 개발 vs 프로덕션

```typescript theme={null}
import { defineConfig } from "sonamu";
import { getConsoleSink, getFileSink } from "@logtape/logtape";
import { getPrettyFormatter } from "@logtape/pretty";

const isDev = process.env.NODE_ENV === "development";

export default defineConfig({
  logging: {
    sinks: {
      console: getConsoleSink({
        formatter: getPrettyFormatter({
          timestamp: isDev ? "time" : "rfc3339",
        }),
      }),
      ...(isDev
        ? {}
        : {
            file: getFileSink("logs/app.log"),
            errorFile: getFileSink("logs/errors.log"),
          }),
    },

    loggers: [
      {
        category: ["fastify"],
        sinks: isDev ? ["console"] : ["console", "file"],
        lowestLevel: isDev ? "debug" : "info",
      },
      ...(!isDev
        ? [
            {
              category: ["fastify"] as const,
              sinks: ["errorFile"] as const,
              lowestLevel: "error" as const,
            },
          ]
        : []),
    ],
  },

  server: {
    listen: { port: 34900 },
  },
});
```

### 다중 Sink & Filter

```typescript theme={null}
import { defineConfig } from "sonamu";
import { getConsoleSink, getFileSink } from "@logtape/logtape";
import type { LogRecord } from "@logtape/logtape";
import type { FastifyRequest, FastifyReply } from "fastify";

export default defineConfig({
  logging: {
    sinks: {
      console: getConsoleSink(),
      accessLog: getFileSink("logs/access.log"),
      errorLog: getFileSink("logs/error.log"),
    },

    filters: {
      "api-requests": (record: LogRecord) => {
        const req = record.properties.req as FastifyRequest | undefined;
        const res = record.properties.res as FastifyReply | undefined;
        const url = req?.url ?? res?.request.url;
        return url?.startsWith("/api") ?? false;
      },
      "errors-only": (record: LogRecord) => record.level >= "error",
    },

    loggers: [
      // 콘솔: 모든 로그
      {
        category: ["fastify"],
        sinks: ["console"],
        lowestLevel: "info",
      },
      // access.log: /api 요청만
      {
        category: ["fastify"],
        sinks: ["accessLog"],
        filters: ["api-requests"],
        lowestLevel: "info",
      },
      // error.log: 에러만
      {
        category: ["fastify"],
        sinks: ["errorLog"],
        filters: ["errors-only"],
        lowestLevel: "error",
      },
    ],
  },

  server: {
    listen: { port: 34900 },
  },
});
```

### 외부 서비스 통합

```typescript theme={null}
import { defineConfig } from "sonamu";
import { getConsoleSink } from "@logtape/logtape";
import type { Sink, LogRecord } from "@logtape/logtape";
import * as Sentry from "@sentry/node";

Sentry.init({ dsn: process.env.SENTRY_DSN });

const sentrySink: Sink = async (record: LogRecord) => {
  if (record.level >= "error") {
    Sentry.captureException(new Error(record.message.join(" ")), {
      level: record.level,
      extra: {
        category: record.category,
        properties: record.properties,
      },
    });
  }
};

export default defineConfig({
  logging: {
    sinks: {
      console: getConsoleSink(),
      sentry: sentrySink,
    },

    loggers: [
      {
        category: ["fastify"],
        sinks: ["console", "sentry"],
        lowestLevel: "info",
      },
    ],
  },

  server: {
    listen: { port: 34900 },
  },
});
```

### URL 패턴별 로깅

```typescript theme={null}
import { defineConfig } from "sonamu";
import { getConsoleSink, getFileSink } from "@logtape/logtape";
import type { LogRecord } from "@logtape/logtape";
import type { FastifyRequest, FastifyReply } from "fastify";

export default defineConfig({
  logging: {
    sinks: {
      console: getConsoleSink(),
      adminLog: getFileSink("logs/admin.log"),
      userLog: getFileSink("logs/user.log"),
    },

    filters: {
      "admin-routes": (record: LogRecord) => {
        const req = record.properties.req as FastifyRequest | undefined;
        const res = record.properties.res as FastifyReply | undefined;
        const url = req?.url ?? res?.request.url;
        return url?.startsWith("/api/admin") ?? false;
      },
      "user-routes": (record: LogRecord) => {
        const req = record.properties.req as FastifyRequest | undefined;
        const res = record.properties.res as FastifyReply | undefined;
        const url = req?.url ?? res?.request.url;
        return url?.startsWith("/api/user") ?? false;
      },
    },

    loggers: [
      {
        category: ["fastify"],
        sinks: ["console", "adminLog"],
        filters: ["admin-routes"],
        lowestLevel: "info",
      },
      {
        category: ["fastify"],
        sinks: ["console", "userLog"],
        filters: ["user-routes"],
        lowestLevel: "info",
      },
    ],
  },

  server: {
    listen: { port: 34900 },
  },
});
```

## LogRecord 구조

Filter와 커스텀 Sink에서 사용하는 `LogRecord` 타입:

```typescript theme={null}
type LogRecord = {
  category: readonly string[];
  level: LogLevel;
  message: unknown[];
  timestamp: Date;
  properties: Record<string, unknown>;
};
```

**주요 프로퍼티:**

* `category`: 로그 카테고리 (예: `["fastify"]`)
* `level`: 로그 레벨 (`"debug"` | `"info"` | `"warning"` | `"error"` | `"fatal"`)
* `message`: 로그 메시지 배열
* `timestamp`: 로그 발생 시간
* `properties`: 추가 정보 (Fastify의 경우 `req`, `res` 등)

**Fastify의 properties:**

```typescript theme={null}
{
  req?: FastifyRequest;
  res?: FastifyReply;
  responseTime?: number;
}
```

## 주의사항

### 1. Sink 이름 충돌

```typescript theme={null}
// ❌ 나쁜 예: 기본 Sink 덮어씀
logging: {
  sinks: {
    "fastify-console": getConsoleSink(),  // 기본 설정 손실
  },
}

// ✅ 좋은 예: 다른 이름 사용
logging: {
  sinks: {
    "my-console": getConsoleSink(),
  },
}
```

### 2. Filter 성능

```typescript theme={null}
// ❌ 나쁜 예: 무거운 작업
filters: {
  "db-check": async (record) => {
    const result = await db.query("...");  // 매 로그마다 DB 조회!
    return result;
  },
}

// ✅ 좋은 예: 빠른 체크
filters: {
  "level-check": (record) => record.level >= "error",
}
```

### 3. 비동기 Sink

```typescript theme={null}
// ✅ Sink는 비동기 가능
const externalSink: Sink = async (record) => {
  await sendToExternalService(record);
};
```

### 4. Filter는 동기만

```typescript theme={null}
// ❌ Filter는 비동기 불가
filters: {
  "async-filter": async (record) => {  // 작동 안 함!
    return await someAsyncCheck();
  },
}

// ✅ 동기 함수만
filters: {
  "sync-filter": (record) => {
    return someCheck(record);
  },
}
```

## 다음 단계

<CardGroup cols={2}>
  <Card title="카테고리 로깅" icon="folder-tree" href="/ko/configuration/logging/category-logging">
    카테고리 시스템으로 로그를 체계적으로 관리하세요
  </Card>

  <Card title="Fastify 로깅" icon="bolt" href="/ko/configuration/logging/fastify-logging">
    HTTP 요청/응답 로깅을 커스터마이징하세요
  </Card>

  <Card title="LogTape 설정" icon="gear" href="/ko/configuration/logging/logtape-setup">
    기본 로깅 설정과 개념을 학습하세요
  </Card>

  <Card title="LogTape 공식 문서" icon="book-open" href="https://github.com/dahlia/logtape">
    LogTape의 고급 기능과 API를 확인하세요
  </Card>
</CardGroup>
