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 ์ฐจ์ด์
๋ ๊ฐ๋
์ ์๋ก ๋ค๋ฅธ ๋ชฉ์ ์ ๊ฐ์ง๋๋ค:
| ๊ตฌ๋ถ | Sink | Filter |
|---|
| ๋ชฉ์ | ์ด๋์ ๊ธฐ๋ก? | ๋ฌด์์ ๊ธฐ๋ก? |
| ํ์ด๋ฐ | ๋ก๊ทธ ์ถ๋ ฅ ์ | ํํฐ๋ง ์ |
| ์์ | ์ฝ์, ํ์ผ, Sentry | URL ํจํด, ๋ ๋ฒจ, ์๊ฐ |
| ์ญํ | ์ถ๋ ฅ ํ์ ๊ฒฐ์ | ์ถ๋ ฅ ์ฌ๋ถ ๊ฒฐ์ |
ํจ๊ป ์ฌ์ฉํ๊ธฐ
{
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);
},
}
๋ค์ ๋จ๊ณ