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:
- 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
- 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)
- 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:
- Remove
ModelClass from class name
- Convert PascalCase → snake_case
- 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:
- Remove
Workflow from class name (suffix only)
- Convert PascalCase → snake_case
- 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:
- Split by
.
- Split each part by
:
- 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:
- Remove suffix from class name (
ModelClass, FrameClass, AgentClass, Workflow)
- Convert PascalCase → snake_case
- Convert snake_case → kebab-case (
_ → -)
- 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:
- Split by
.
- Split each part by
:
- 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