Skip to main content
LogTape structures logs with a category system. Sonamu automatically generates categories for Model, Frame, Workflow, and Agent classes.

What is a Category?

A category represents the source of logs in a hierarchical structure. Format: ["a", "b", "c"] array Examples:
  • ["fastify"] - Fastify framework
  • ["sonamu", "model", "user-model"] - UserModel class
  • ["sonamu", "workflow", "email-send"] - EmailSendWorkflow
  • ["app", "payment", "processor"] - Custom category

Purpose of the Category System

Categories are needed to systematically manage logs. 1. Log Source Identification
// Where did the log originate?
["fastify"]                    // HTTP request
["sonamu", "model"]            // Model layer
["app", "payment"]             // Payment logic
2. Selective Logging
// Log only specific parts
loggers: [
  {
    category: ["sonamu", "model"],  // Only Model at debug level
    lowestLevel: "debug",
  },
  {
    category: ["fastify"],          // Fastify at info level
    lowestLevel: "info",
  },
]
3. Separate Log Storage
// Different files for different parts
loggers: [
  {
    category: ["sonamu", "model"],
    sinks: ["modelLog"],           // models.log
  },
  {
    category: ["app", "payment"],
    sinks: ["paymentLog"],         // payment.log
  },
]

Benefits of Hierarchical Structure

Why categories are hierarchical: 1. Clear Structure
["sonamu", "model", "user-model"]
//  ↓        ↓        ↓
//  framework  type    specific name
You can see at a glance where a log originated 2. Flexible Filtering
// All Sonamu logs
category: ["sonamu"]

// Only Model layer
category: ["sonamu", "model"]

// Only UserModel
category: ["sonamu", "model", "user-model"]
3. Collision Prevention
// Can distinguish "user" logs from different projects
["sonamu", "model", "user-model"]  // Sonamu's user
["app", "user", "service"]          // Application's user
["external", "api", "user"]        // External API's user

Why Only Exact Matching?

LogTape only matches categories that exactly match.
// ❌ Partial matching not supported
category: ["sonamu"]
// → Logs from ["sonamu", "model", "user-model"] won't match

// ✅ Must match exactly
category: ["sonamu", "model", "user-model"]
// → Only matches ["sonamu", "model", "user-model"] logs
Reasons:
  1. Clarity
// If partial matching was allowed?
category: ["sonamu"]
// → ["sonamu", "model", "user-model"]
// → ["sonamu", "workflow", "email-send"]
// → ["sonamu", "agent", "payment-agent"]
// → Too many logs would match!

// Exact matching
category: ["sonamu", "model", "user-model"]
// → Matches exactly the logs you want
  1. Performance
// If wildcard matching was allowed?
category: ["sonamu", "*"]  // All sonamu subcategories
// → Pattern matching required every time (slow)

// Exact matching
category: ["sonamu", "model"]
// → Simple string comparison (fast)
  1. Predictability
// Confusion with partial matching
loggers: [
  { category: ["sonamu"], lowestLevel: "info" },
  { category: ["sonamu", "model"], lowestLevel: "debug" },
]
// → Where would ["sonamu", "model", "user-model"] logs match?

// Clear with exact matching
loggers: [
  { category: ["sonamu", "model"], lowestLevel: "debug" },
]
// → Only matches ["sonamu", "model"], very clear!

Category Structure Details

Hierarchy Representation

Categories narrow down progressively.
["sonamu", "model", "user-model"]
//  ↓        ↓        ↓
//  namespace  type    specific name
Configuration example:
export default defineConfig({
  logging: {
    loggers: [
      // All logs starting with "sonamu"
      {
        category: ["sonamu"],
        sinks: ["console"],
        lowestLevel: "info",
      },
      
      // Model only
      {
        category: ["sonamu", "model"],
        sinks: ["modelLog"],
        lowestLevel: "debug",
      },
      
      // UserModel only
      {
        category: ["sonamu", "model", "user-model"],
        sinks: ["userLog"],
        lowestLevel: "debug",
      },
    ],
  },
  
  server: {
    // ...
  },
});

Category Matching

LogTape only matches categories that exactly match.
// ❌ Partial matching not supported
category: ["sonamu"]
// → Logs from ["sonamu", "model", "user-model"] won't match

// ✅ Must match exactly
category: ["sonamu", "model", "user-model"]
// → Only matches ["sonamu", "model", "user-model"] logs

Automatic Category Generation

Sonamu automatically generates categories for specific classes.

Model Classes

// UserModelClass → ["sonamu", "model", "user-model"]
// PostModelClass → ["sonamu", "model", "post-model"]
// OrderItemModelClass → ["sonamu", "model", "order-item-model"]
Conversion rules:
  1. Remove ModelClass from class name
  2. Convert PascalCase → snake_case
  3. Convert snake_case → kebab-case
Examples:
UserModelClass
→ User (remove ModelClass)
→ user (snake_case)
→ ["sonamu", "model", "user"] (kebab-case, add prefix)

OrderItemModelClass
→ OrderItem
→ order_item
→ ["sonamu", "model", "order-item"]

Frame Classes

// UserFrameClass → ["sonamu", "frame", "user-frame"]
// AdminUserFrameClass → ["sonamu", "frame", "admin-user-frame"]
Conversion rules: Same as Model but removes FrameClass

Workflow

// EmailSendWorkflow → ["sonamu", "workflow", "email-send"]
// OrderProcessWorkflow → ["sonamu", "workflow", "order-process"]
Conversion rules:
  1. Remove Workflow from class name (suffix only)
  2. Convert PascalCase → snake_case
  3. Convert snake_case → kebab-case
Examples:
EmailSendWorkflow
→ EmailSend (remove Workflow)
→ email_send (snake_case)
→ ["sonamu", "workflow", "email-send"] (kebab-case, add prefix)

Agent Classes

// PaymentAgentClass → ["sonamu", "agent", "payment-agent"]
// AdminNotificationAgentClass → ["sonamu", "agent", "admin-notification-agent"]
Conversion rules: Same as Model but removes AgentClass

Naite Categories

Naite test keys follow special category conversion rules.
// "user.findOne" → ["user", "findOne"]
// "user:list" → ["user", "list"]
// "admin.company:detail" → ["admin", "company", "detail"]
Conversion rules:
  1. Split by .
  2. Split each part by :
  3. Flatten
Examples:
"user.findOne"
→ ["user", "findOne"]

"admin.company:detail"
→ split by "." → ["admin", "company:detail"]
→ split by ":" → [["admin"], ["company", "detail"]]
→ flatten → ["admin", "company", "detail"]

Category-Based Logging Configuration

Separate Logs by Model

import { defineConfig } from "sonamu";
import { getFileSink } from "@logtape/logtape";

export default defineConfig({
  logging: {
    sinks: {
      userLog: getFileSink("logs/user-model.log"),
      postLog: getFileSink("logs/post-model.log"),
    },
    
    loggers: [
      {
        category: ["sonamu", "model", "user-model"],
        sinks: ["userLog"],
        lowestLevel: "debug",
      },
      {
        category: ["sonamu", "model", "post-model"],
        sinks: ["postLog"],
        lowestLevel: "debug",
      },
    ],
  },
  
  server: {
    // ...
  },
});

Adjust Log Levels by Type

export default defineConfig({
  logging: {
    loggers: [
      // Model: debug level
      {
        category: ["sonamu", "model"],
        sinks: ["console"],
        lowestLevel: "debug",
      },
      
      // Workflow: info level
      {
        category: ["sonamu", "workflow"],
        sinks: ["console"],
        lowestLevel: "info",
      },
      
      // Agent: warning level
      {
        category: ["sonamu", "agent"],
        sinks: ["console"],
        lowestLevel: "warning",
      },
    ],
  },
  
  server: {
    // ...
  },
});

Management by Namespace

export default defineConfig({
  logging: {
    sinks: {
      sonamuLog: getFileSink("logs/sonamu.log"),
      appLog: getFileSink("logs/app.log"),
    },
    
    loggers: [
      // Sonamu internal
      {
        category: ["sonamu"],
        sinks: ["sonamuLog"],
        lowestLevel: "info",
      },
      
      // Application
      {
        category: ["app"],
        sinks: ["appLog"],
        lowestLevel: "debug",
      },
    ],
  },
  
  server: {
    // ...
  },
});

Custom Categories

You can use custom categories in application code.
import { getLogger } from "@logtape/logtape";

// Create logger
const logger = getLogger(["app", "payment", "processor"]);

// Logging
logger.info("Processing payment", { orderId: 123 });
logger.error("Payment failed", { error: "timeout" });
Configuration:
export default defineConfig({
  logging: {
    sinks: {
      paymentLog: getFileSink("logs/payment.log"),
    },
    
    loggers: [
      {
        category: ["app", "payment", "processor"],
        sinks: ["paymentLog"],
        lowestLevel: "info",
      },
    ],
  },
  
  server: {
    // ...
  },
});

Practical Examples

Development Environment Detailed Logging

import { defineConfig } from "sonamu";
import { getConsoleSink } from "@logtape/logtape";

export default defineConfig({
  logging: {
    sinks: {
      console: getConsoleSink(),
    },
    
    loggers: [
      // Fastify: info
      {
        category: ["fastify"],
        sinks: ["console"],
        lowestLevel: "info",
      },
      
      // All Sonamu internal: debug
      {
        category: ["sonamu"],
        sinks: ["console"],
        lowestLevel: "debug",
      },
      
      // App logic: debug
      {
        category: ["app"],
        sinks: ["console"],
        lowestLevel: "debug",
      },
    ],
  },
  
  server: {
    listen: { port: 1028 },
  },
});

Production Selective Logging

import { defineConfig } from "sonamu";
import { getConsoleSink, getFileSink } from "@logtape/logtape";

export default defineConfig({
  logging: {
    sinks: {
      console: getConsoleSink(),
      workflowLog: getFileSink("logs/workflow.log"),
      errorLog: getFileSink("logs/error.log"),
    },
    
    loggers: [
      // Fastify: warning and above
      {
        category: ["fastify"],
        sinks: ["console"],
        lowestLevel: "warning",
      },
      
      // Workflow: full recording
      {
        category: ["sonamu", "workflow"],
        sinks: ["workflowLog"],
        lowestLevel: "info",
      },
      
      // Errors only to separate file
      {
        category: ["sonamu"],
        sinks: ["errorLog"],
        lowestLevel: "error",
      },
    ],
  },
  
  server: {
    listen: { port: 1028 },
  },
});

Log Separation by Domain

import { defineConfig } from "sonamu";
import { getFileSink } from "@logtape/logtape";

export default defineConfig({
  logging: {
    sinks: {
      userDomain: getFileSink("logs/user-domain.log"),
      orderDomain: getFileSink("logs/order-domain.log"),
      paymentDomain: getFileSink("logs/payment-domain.log"),
    },
    
    loggers: [
      // User related
      {
        category: ["sonamu", "model", "user-model"],
        sinks: ["userDomain"],
        lowestLevel: "debug",
      },
      {
        category: ["app", "user"],
        sinks: ["userDomain"],
        lowestLevel: "debug",
      },
      
      // Order related
      {
        category: ["sonamu", "model", "order-model"],
        sinks: ["orderDomain"],
        lowestLevel: "debug",
      },
      {
        category: ["app", "order"],
        sinks: ["orderDomain"],
        lowestLevel: "debug",
      },
      
      // Payment related
      {
        category: ["app", "payment"],
        sinks: ["paymentDomain"],
        lowestLevel: "debug",
      },
    ],
  },
  
  server: {
    listen: { port: 1028 },
  },
});

Internal Operation Principles

This section explains how Sonamu’s category system works internally. The functions shown here are used automatically by Sonamu and cannot be called directly by users.

isSameCategory()

An internal function that checks if two categories match exactly.
// Sonamu internal function (not directly usable)
isSameCategory(["sonamu", "model"], ["sonamu", "model"]);
// → true

isSameCategory(["sonamu", "model"], ["sonamu", "workflow"]);
// → false

isSameCategory(["sonamu"], ["sonamu", "model"]);
// → false (different lengths)
Internal use: Used when matching categories in LogTape’s logger configuration.

convertDomainToCategory()

An internal function that converts class names to categories.
// Sonamu internal function (not directly usable)
convertDomainToCategory("UserModelClass", "model");
// → ["sonamu", "model", "user-model"]

convertDomainToCategory("EmailSendWorkflow", "workflow");
// → ["sonamu", "workflow", "email-send"]

convertDomainToCategory("PaymentAgentClass", "agent");
// → ["sonamu", "agent", "payment-agent"]
Internal use: Automatically assigns categories when Model, Frame, Workflow, and Agent classes are created. Conversion logic:
  1. Remove suffix from class name (ModelClass, FrameClass, AgentClass, Workflow)
  2. Convert PascalCase → snake_case
  3. Convert snake_case → kebab-case (_-)
  4. Compose as ["sonamu", type, name] format

convertNaiteKeyToCategory()

An internal function that converts Naite test keys to categories.
// Sonamu internal function (not directly usable)
convertNaiteKeyToCategory("user.findOne");
// → ["user", "findOne"]

convertNaiteKeyToCategory("admin.company:detail");
// → ["admin", "company", "detail"]
Internal use: Automatically generates log categories when running Naite tests. Conversion logic:
  1. Split by .
  2. Split each part by :
  3. Flatten

Important Notes

1. Only Exact Matching Supported

// ❌ No wildcards
category: ["sonamu", "*"]  // Won't work

// ❌ No partial matching
category: ["sonamu"]  // Won't match ["sonamu", "model"]

// ✅ Exact categories only
category: ["sonamu", "model", "user-model"]

2. Categories are readonly Arrays

// ✅ Use readonly arrays
const category: readonly string[] = ["app", "payment"];

// ❌ Regular arrays may cause type errors
const category: string[] = ["app", "payment"];

3. Class Naming Conventions

// ✅ Correct names
class UserModelClass { }          // → ["sonamu", "model", "user-model"]
class OrderItemModelClass { }     // → ["sonamu", "model", "order-item-model"]
class EmailSendWorkflow { }       // → ["sonamu", "workflow", "email-send"]

// ❌ Convention violations (won't auto-convert)
class User { }                    // Not a Model
class UserModel { }               // Missing "Class" suffix
class WorkflowEmailSend { }       // "Workflow" as prefix (should be suffix)

4. Fastify Category Duplication

// ❌ Conflict with Fastify category
fastifyCategory: ["sonamu", "model"]

loggers: [
  {
    category: ["sonamu", "model"],  // Mixed with Fastify logs!
    // ...
  },
]

// ✅ Separate categories
fastifyCategory: ["fastify"]

loggers: [
  {
    category: ["sonamu", "model"],
    // ...
  },
]

Next Steps