๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Sink๋Š” ๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•  ๋Œ€์ƒ์„, Filter๋Š” ์–ด๋–ค ๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ• ์ง€ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. sonamu.config.ts์˜ logging ์˜ต์…˜์—์„œ ์ปค์Šคํ…€ sink์™€ filter๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Sink๋ž€?

Sink๋Š” ๋กœ๊ทธ๊ฐ€ ์ตœ์ข…์ ์œผ๋กœ ์ถœ๋ ฅ๋˜๋Š” ๊ณณ์ž…๋‹ˆ๋‹ค. ์˜ˆ์‹œ:
  • ์ฝ˜์†” (ํ„ฐ๋ฏธ๋„)
  • ํŒŒ์ผ
  • ์™ธ๋ถ€ ๋กœ๊น… ์„œ๋น„์Šค (Sentry, Datadog ๋“ฑ)
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

Sink๊ฐ€ ํ•„์š”ํ•œ ์ด์œ 

๋กœ๊ทธ๋Š” ๋ชฉ์ ์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๊ณณ์— ๊ธฐ๋ก๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: 1. ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง (์ฝ˜์†”)
// ๊ฐœ๋ฐœ ์ค‘ ์ฆ‰์‹œ ํ™•์ธ
logger.info("API called", { url: "/api/user" });
// โ†’ ํ„ฐ๋ฏธ๋„์— ๋ฐ”๋กœ ์ถœ๋ ฅ
2. ์˜๊ตฌ ๋ณด๊ด€ (ํŒŒ์ผ)
// ๋‚˜์ค‘์— ๋ถ„์„ํ•˜๊ธฐ ์œ„ํ•ด
logger.error("Payment failed", { orderId });
// โ†’ errors.log ํŒŒ์ผ์— ์ €์žฅ
3. ์—๋Ÿฌ ์•Œ๋ฆผ (์™ธ๋ถ€ ์„œ๋น„์Šค)
// ์‹ฌ๊ฐํ•œ ๋ฌธ์ œ ์ฆ‰์‹œ ํ†ต๋ณด
logger.fatal("DB connection lost");
// โ†’ Sentry์— ์ „์†ก, ์Šฌ๋ž™ ์•Œ๋ฆผ
4. ํ†ต๊ณ„ ๋ถ„์„ (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค)
// ๋กœ๊ทธ ๋ฐ์ดํ„ฐ ๋ถ„์„
logger.info("User action", { action: "purchase", amount });
// โ†’ DB์— ์ €์žฅ, ๋‚˜์ค‘์— ์ง‘๊ณ„

์—ฌ๋Ÿฌ Sink ๋™์‹œ ์‚ฌ์šฉ

ํ•œ ๋กœ๊ทธ๋ฅผ ์—ฌ๋Ÿฌ Sink์— ๋™์‹œ์— ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
loggers: [
  {
    category: ["fastify"],
    sinks: ["console", "file", "sentry"],  // 3๊ณณ์— ๋™์‹œ ๊ธฐ๋ก
    lowestLevel: "error",
  },
]
์žฅ์ :
  • ์‹ค์‹œ๊ฐ„ ํ™•์ธ (console)
  • ์˜๊ตฌ ๋ณด๊ด€ (file)
  • ์ฆ‰์‹œ ์•Œ๋ฆผ (sentry)

Filter๋ž€?

Filter๋Š” ๋กœ๊ทธ๋ฅผ ์„ ํƒ์ ์œผ๋กœ ์ถœ๋ ฅํ•˜๊ธฐ ์œ„ํ•œ ์กฐ๊ฑด์ž…๋‹ˆ๋‹ค. ์˜ˆ์‹œ:
  • ํŠน์ • ์นดํ…Œ๊ณ ๋ฆฌ๋งŒ
  • ํŠน์ • ๋ ˆ๋ฒจ ์ด์ƒ๋งŒ
  • ํŠน์ • URL ํŒจํ„ด๋งŒ
  • ์‹œ๊ฐ„๋Œ€๋ณ„๋กœ

Filter๊ฐ€ ํ•„์š”ํ•œ ์ด์œ 

๋ชจ๋“  ๋กœ๊ทธ๋ฅผ ๋‹ค ๊ธฐ๋กํ•˜๋ฉด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค: 1. ์„ฑ๋Šฅ ์ €ํ•˜
// Filter ์—†์Œ: ๋ชจ๋“  ์š”์ฒญ ๋กœ๊น…
/favicon.ico  // ๋ถˆํ•„์š”
/assets/logo.png  // ๋ถˆํ•„์š”
/api/healthcheck  // ๋ฐ˜๋ณต์ , ๋ถˆํ•„์š”
/api/user/list  // ํ•„์š”!

// Filter ์žˆ์Œ: /api๋งŒ ๋กœ๊น…
/api/user/list  // ํ•„์š”!
2. ๋””์Šคํฌ ๊ณต๊ฐ„ ๋‚ญ๋น„
// ํ•˜๋ฃจ 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 ์‚ฌ์šฉ ์˜ˆ์‹œ

filters: {
  // ์ค‘์š”ํ•œ ๊ฒƒ๋งŒ
  "important": (record) => {
    // API ์š”์ฒญ๋งŒ
    if (!record.url.startsWith("/api")) return false;
    // healthcheck ์ œ์™ธ
    if (record.url === "/api/healthcheck") return false;
    return true;
  },
}

Sink vs Filter ์ฐจ์ด์ 

๋‘ ๊ฐœ๋…์€ ์„œ๋กœ ๋‹ค๋ฅธ ๋ชฉ์ ์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค:
๊ตฌ๋ถ„SinkFilter
๋ชฉ์ ์–ด๋””์— ๊ธฐ๋ก?๋ฌด์—‡์„ ๊ธฐ๋ก?
ํƒ€์ด๋ฐ๋กœ๊ทธ ์ถœ๋ ฅ ์‹œํ•„ํ„ฐ๋ง ์‹œ
์˜ˆ์‹œ์ฝ˜์†”, ํŒŒ์ผ, SentryURL ํŒจํ„ด, ๋ ˆ๋ฒจ, ์‹œ๊ฐ„
์—ญํ• ์ถœ๋ ฅ ํ˜•์‹ ๊ฒฐ์ •์ถœ๋ ฅ ์—ฌ๋ถ€ ๊ฒฐ์ •

ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ

{
  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. ์ƒˆ๋กœ์šด ์ถœ๋ ฅ ๋Œ€์ƒ์ด ํ•„์š”ํ•  ๋•Œ
// ์˜ˆ: Slack์œผ๋กœ ์—๋Ÿฌ ์•Œ๋ฆผ
sinks: {
  slack: customSlackSink,
}
2. ๋‹ค๋ฅธ ํ˜•์‹์œผ๋กœ ๊ธฐ๋กํ•  ๋•Œ
// ์˜ˆ: JSON ํ˜•์‹์œผ๋กœ ํŒŒ์ผ ์ €์žฅ
sinks: {
  jsonFile: getFileSink("app.json", { formatter: jsonFormatter }),
}
3. ์—ฌ๋Ÿฌ ๊ณณ์— ๋™์‹œ ๊ธฐ๋กํ•  ๋•Œ
loggers: [
  {
    sinks: ["console", "file", "sentry"],  // 3๊ณณ์— ๋™์‹œ
  },
]

Filter๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ

1. ํŠน์ • ์กฐ๊ฑด์˜ ๋กœ๊ทธ๋งŒ ํ•„์š”ํ•  ๋•Œ
// ์˜ˆ: ๋А๋ฆฐ ์š”์ฒญ๋งŒ
filters: {
  "slow": (record) => record.responseTime > 1000,
}
2. ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•˜๊ณ  ์‹ถ์„ ๋•Œ
// ์˜ˆ: ๋ถˆํ•„์š”ํ•œ ๋กœ๊ทธ ์ œ์™ธ
filters: {
  "no-healthcheck": (record) => record.url !== "/api/healthcheck",
}
3. Sink๋ณ„๋กœ ๋‹ค๋ฅธ ๋กœ๊ทธ๋ฅผ ๋ณด๋‚ด๊ณ  ์‹ถ์„ ๋•Œ
loggers: [
  {
    sinks: ["console"],
    filters: ["all"],     // ์ฝ˜์†”: ๋ชจ๋“  ๋กœ๊ทธ
  },
  {
    sinks: ["errorFile"],
    filters: ["errors"],  // ํŒŒ์ผ: ์—๋Ÿฌ๋งŒ
  },
]

Sinks ์„ค์ • ์ƒ์„ธ

sinks ์˜ต์…˜

logging.sinks์— ์ปค์Šคํ…€ Sink๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: Record<string, Sink>
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: {
    // ...
  },
});
"fastify-console" ์ด๋ฆ„์€ Sonamu๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜๋Š” ๊ธฐ๋ณธ Sink์ž…๋‹ˆ๋‹ค. ์ด ์ด๋ฆ„์œผ๋กœ ์ถ”๊ฐ€ํ•˜๋ฉด ๊ธฐ๋ณธ Sink๋ฅผ ๋ฎ์–ด์”๋‹ˆ๋‹ค.

์ฝ˜์†” Sink

ํ„ฐ๋ฏธ๋„์— ๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
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

ํŒŒ์ผ์— ๋กœ๊ทธ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
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 ์ŠคํŠธ๋ฆผ์— ๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค.
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๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
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>
type FilterLike = Filter | string;
type Filter = (record: LogRecord) => boolean;
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: {
    // ...
  },
});
"fastify-console" ์ด๋ฆ„์€ Sonamu๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜๋Š” ๊ธฐ๋ณธ Filter์ž…๋‹ˆ๋‹ค. ์ด ์ด๋ฆ„์œผ๋กœ ์ถ”๊ฐ€ํ•˜๋ฉด ๊ธฐ๋ณธ Filter๋ฅผ ๋ฎ์–ด์”๋‹ˆ๋‹ค.

URL ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ

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: {
    // ...
  },
});

๋ ˆ๋ฒจ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ

export default defineConfig({
  logging: {
    filters: {
      "error-only": (record) => record.level >= "error",
      "warning-or-higher": (record) => record.level >= "warning",
    },
  },
  
  server: {
    // ...
  },
});

์‹œ๊ฐ„ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ

export default defineConfig({
  logging: {
    filters: {
      "business-hours": (record) => {
        const hour = new Date().getHours();
        return hour >= 9 && hour < 18;  // 09:00 - 18:00๋งŒ
      },
    },
  },
  
  server: {
    // ...
  },
});

ํ”„๋กœํผํ‹ฐ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ

export default defineConfig({
  logging: {
    filters: {
      "slow-requests": (record) => {
        const responseTime = record.properties.responseTime as number | undefined;
        return responseTime ? responseTime > 1000 : false;  // 1์ดˆ ์ด์ƒ๋งŒ
      },
    },
  },
  
  server: {
    // ...
  },
});

์‹ค์ „ ์˜ˆ์‹œ

๊ฐœ๋ฐœ vs ํ”„๋กœ๋•์…˜

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: 1028 },
  },
});

๋‹ค์ค‘ Sink & Filter

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: 1028 },
  },
});

์™ธ๋ถ€ ์„œ๋น„์Šค ํ†ตํ•ฉ

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: 1028 },
  },
});

URL ํŒจํ„ด๋ณ„ ๋กœ๊น…

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: 1028 },
  },
});

LogRecord ๊ตฌ์กฐ

Filter์™€ ์ปค์Šคํ…€ Sink์—์„œ ์‚ฌ์šฉํ•˜๋Š” LogRecord ํƒ€์ž…:
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:
{
  req?: FastifyRequest;
  res?: FastifyReply;
  responseTime?: number;
}

์ฃผ์˜์‚ฌํ•ญ

1. Sink ์ด๋ฆ„ ์ถฉ๋Œ

// โŒ ๋‚˜์œ ์˜ˆ: ๊ธฐ๋ณธ Sink ๋ฎ์–ด์”€
logging: {
  sinks: {
    "fastify-console": getConsoleSink(),  // ๊ธฐ๋ณธ ์„ค์ • ์†์‹ค
  },
}

// โœ… ์ข‹์€ ์˜ˆ: ๋‹ค๋ฅธ ์ด๋ฆ„ ์‚ฌ์šฉ
logging: {
  sinks: {
    "my-console": getConsoleSink(),
  },
}

2. Filter ์„ฑ๋Šฅ

// โŒ ๋‚˜์œ ์˜ˆ: ๋ฌด๊ฑฐ์šด ์ž‘์—…
filters: {
  "db-check": async (record) => {
    const result = await db.query("...");  // ๋งค ๋กœ๊ทธ๋งˆ๋‹ค DB ์กฐํšŒ!
    return result;
  },
}

// โœ… ์ข‹์€ ์˜ˆ: ๋น ๋ฅธ ์ฒดํฌ
filters: {
  "level-check": (record) => record.level >= "error",
}

3. ๋น„๋™๊ธฐ Sink

// โœ… Sink๋Š” ๋น„๋™๊ธฐ ๊ฐ€๋Šฅ
const externalSink: Sink = async (record) => {
  await sendToExternalService(record);
};

4. Filter๋Š” ๋™๊ธฐ๋งŒ

// โŒ Filter๋Š” ๋น„๋™๊ธฐ ๋ถˆ๊ฐ€
filters: {
  "async-filter": async (record) => {  // ์ž‘๋™ ์•ˆ ํ•จ!
    return await someAsyncCheck();
  },
}

// โœ… ๋™๊ธฐ ํ•จ์ˆ˜๋งŒ
filters: {
  "sync-filter": (record) => {
    return someCheck(record);
  },
}

๋‹ค์Œ ๋‹จ๊ณ„