Skip to main content
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:
AspectSinkFilter
PurposeWhere to record?What to record?
TimingAt log outputAt filtering
ExamplesConsole, file, SentryURL patterns, levels, time
RoleDetermine output formatDetermine 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(),
  },
}

2. Filter Performance

// ❌ 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