Sinks determine where logs are output, and Filters determine which logs to output. You can add custom sinks and filters in the logging option of sonamu.config.ts.
What is a Sink?
A Sink is where logs are ultimately output.
Examples:
- Console (terminal)
- Files
- External logging services (Sentry, Datadog, etc.)
- Database
Why Sinks are Needed
Logs should be recorded in different places depending on their purpose:
1. Real-time Monitoring (Console)
// Check immediately during development
logger.info("API called", { url: "/api/user" });
// β Output directly to terminal
2. Permanent Storage (File)
// For later analysis
logger.error("Payment failed", { orderId });
// β Save to errors.log file
3. Error Notifications (External Service)
// Immediate notification for serious issues
logger.fatal("DB connection lost");
// β Send to Sentry, Slack notification
4. Statistical Analysis (Database)
// Log data analysis
logger.info("User action", { action: "purchase", amount });
// β Store in DB, aggregate later
Using Multiple Sinks Simultaneously
You can send a single log to multiple Sinks at once:
loggers: [
{
category: ["fastify"],
sinks: ["console", "file", "sentry"], // Record to 3 places simultaneously
lowestLevel: "error",
},
]
Benefits:
- Real-time viewing (console)
- Permanent storage (file)
- Immediate alerts (sentry)
What is a Filter?
A Filter is a condition for selectively outputting logs.
Examples:
- Only specific categories
- Only above a certain level
- Only specific URL patterns
- By time period
Why Filters are Needed
Recording all logs causes problems:
1. Performance Degradation
// No Filter: logging all requests
/favicon.ico // Unnecessary
/assets/logo.png // Unnecessary
/api/healthcheck // Repetitive, unnecessary
/api/user/list // Needed!
// With Filter: logging only /api
/api/user/list // Needed!
2. Disk Space Waste
// 1 million requests per day
// No Filter: record all 1 million β hundreds of MB
// With Filter: record only 100,000 β tens of MB
3. Readability Degradation
[Log file]
[INFO] /favicon.ico
[INFO] /assets/style.css
[INFO] /assets/logo.png
[INFO] /api/healthcheck
[INFO] /api/healthcheck
[INFO] /api/user/list // β Hard to find needed logs!
[INFO] /api/healthcheck
Filter Usage Example
filters: {
// Only important ones
"important": (record) => {
// Only API requests
if (!record.url.startsWith("/api")) return false;
// Exclude healthcheck
if (record.url === "/api/healthcheck") return false;
return true;
},
}
Sink vs Filter Differences
The two concepts have different purposes:
| Aspect | Sink | Filter |
|---|
| Purpose | Where to record? | What to record? |
| Timing | At log output | At filtering |
| Examples | Console, file, Sentry | URL patterns, levels, time |
| Role | Determine output format | Determine whether to output |
Using Them Together
{
sinks: {
console: getConsoleSink(), // Where?
errorFile: getFileSink("..."), // Where?
},
filters: {
"api-only": (record) => ..., // What?
"errors": (record) => ..., // What?
},
loggers: [
{
category: ["fastify"],
sinks: ["console"], // To console
filters: ["api-only"], // Only API logs
},
{
category: ["fastify"],
sinks: ["errorFile"], // To errorFile
filters: ["errors"], // Only error logs
},
],
}
When to Use?
When to Add a Sink
1. When you need a new output destination
// Example: Send error notifications to Slack
sinks: {
slack: customSlackSink,
}
2. When recording in a different format
// Example: Save to file in JSON format
sinks: {
jsonFile: getFileSink("app.json", { formatter: jsonFormatter }),
}
3. When recording to multiple places simultaneously
loggers: [
{
sinks: ["console", "file", "sentry"], // To 3 places simultaneously
},
]
When to Add a Filter
1. When you only need logs matching specific conditions
// Example: Only slow requests
filters: {
"slow": (record) => record.responseTime > 1000,
}
2. When you want to improve performance
// Example: Exclude unnecessary logs
filters: {
"no-healthcheck": (record) => record.url !== "/api/healthcheck",
}
3. When you want to send different logs to different Sinks
loggers: [
{
sinks: ["console"],
filters: ["all"], // Console: all logs
},
{
sinks: ["errorFile"],
filters: ["errors"], // File: errors only
},
]
Sinks Configuration Details
sinks Option
Add custom Sinks to logging.sinks.
Type: 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: {
// ...
},
});
The name "fastify-console" is the default Sink automatically generated by Sonamu. Adding with this name will overwrite the default Sink.
Console Sink
Outputs logs to the terminal.
import { getConsoleSink } from "@logtape/logtape";
import { getPrettyFormatter } from "@logtape/pretty";
export default defineConfig({
logging: {
sinks: {
console: getConsoleSink({
formatter: getPrettyFormatter({
timestamp: "time",
categoryWidth: 20,
}),
}),
},
},
server: {
// ...
},
});
File Sink
Saves logs to a file.
import { getFileSink } from "@logtape/logtape";
export default defineConfig({
logging: {
sinks: {
accessLog: getFileSink("logs/access.log"),
errorLog: getFileSink("logs/errors.log"),
},
loggers: [
// All logs to access.log
{
category: ["fastify"],
sinks: ["accessLog"],
lowestLevel: "info",
},
// Errors only to errors.log
{
category: ["fastify"],
sinks: ["errorLog"],
lowestLevel: "error",
},
],
},
server: {
// ...
},
});
Stream Sink
Outputs logs to a Node.js stream.
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: {
// ...
},
});
Custom Sink
You can implement your own 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"], // Console + Sentry
lowestLevel: "info",
},
],
},
server: {
// ...
},
});
Filters Configuration
filters Option
Selectively filter logs.
Type: 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: {
// ...
},
});
The name "fastify-console" is the default Filter automatically generated by Sonamu. Adding with this name will overwrite the default Filter.
URL-Based Filter
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/* only
lowestLevel: "info",
},
],
},
server: {
// ...
},
});
Level-Based Filter
export default defineConfig({
logging: {
filters: {
"error-only": (record) => record.level >= "error",
"warning-or-higher": (record) => record.level >= "warning",
},
},
server: {
// ...
},
});
Time-Based Filter
export default defineConfig({
logging: {
filters: {
"business-hours": (record) => {
const hour = new Date().getHours();
return hour >= 9 && hour < 18; // 09:00 - 18:00 only
},
},
},
server: {
// ...
},
});
Property-Based Filter
export default defineConfig({
logging: {
filters: {
"slow-requests": (record) => {
const responseTime = record.properties.responseTime as number | undefined;
return responseTime ? responseTime > 1000 : false; // Over 1 second only
},
},
},
server: {
// ...
},
});
Practical Examples
Development vs Production
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 },
},
});
Multiple Sinks & Filters
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: [
// Console: all logs
{
category: ["fastify"],
sinks: ["console"],
lowestLevel: "info",
},
// access.log: /api requests only
{
category: ["fastify"],
sinks: ["accessLog"],
filters: ["api-requests"],
lowestLevel: "info",
},
// error.log: errors only
{
category: ["fastify"],
sinks: ["errorLog"],
filters: ["errors-only"],
lowestLevel: "error",
},
],
},
server: {
listen: { port: 1028 },
},
});
External Service Integration
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 },
},
});
Logging by URL Pattern
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 Structure
The LogRecord type used in Filters and custom Sinks:
type LogRecord = {
category: readonly string[];
level: LogLevel;
message: unknown[];
timestamp: Date;
properties: Record<string, unknown>;
};
Key properties:
category: Log category (e.g., ["fastify"])
level: Log level ("debug" | "info" | "warning" | "error" | "fatal")
message: Log message array
timestamp: Time the log occurred
properties: Additional information (for Fastify: req, res, etc.)
Fastifyβs properties:
{
req?: FastifyRequest;
res?: FastifyReply;
responseTime?: number;
}
Important Notes
1. Sink Name Conflicts
// β Bad example: Overwrites default Sink
logging: {
sinks: {
"fastify-console": getConsoleSink(), // Loses default settings
},
}
// β
Good example: Use different name
logging: {
sinks: {
"my-console": getConsoleSink(),
},
}
// β Bad example: Heavy operations
filters: {
"db-check": async (record) => {
const result = await db.query("..."); // DB query for every log!
return result;
},
}
// β
Good example: Fast check
filters: {
"level-check": (record) => record.level >= "error",
}
3. Async Sinks
// β
Sinks can be async
const externalSink: Sink = async (record) => {
await sendToExternalService(record);
};
4. Filters are Sync Only
// β Filters cannot be async
filters: {
"async-filter": async (record) => { // Won't work!
return await someAsyncCheck();
},
}
// β
Sync functions only
filters: {
"sync-filter": (record) => {
return someCheck(record);
},
}
Next Steps