๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Sonamu์˜ LogTape ๊ธฐ๋ฐ˜ ๋กœ๊น… ์‹œ์Šคํ…œ์„ ํ™œ์šฉํ•œ ๋””๋ฒ„๊น… ๋ฐฉ๋ฒ•์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

LogTape๋ž€?

Sonamu๋Š” @logtape/logtape๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌ์กฐํ™”๋œ ๋กœ๊น…์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ฃผ์š” ํŠน์ง•:
  • ์นดํ…Œ๊ณ ๋ฆฌ ๊ธฐ๋ฐ˜ ๋กœ๊น…
  • ๋‹ค์–‘ํ•œ ๋กœ๊ทธ ๋ ˆ๋ฒจ (debug, info, warn, error, fatal)
  • ์ปค์Šคํ…€ Sink/Filter ์ง€์›
  • Fastify ํ†ตํ•ฉ

๊ธฐ๋ณธ ๋กœ๊น…

getLogger ์‚ฌ์šฉ

import { getLogger } from "@logtape/logtape";

const logger = getLogger(["myapp", "user"]);

logger.info("์‚ฌ์šฉ์ž ์ƒ์„ฑ ์‹œ์ž‘", { email: "[email protected]" });
logger.debug("์ƒ์„ธ ์ •๋ณด", { step: 1, data: someData });
logger.error("์—๋Ÿฌ ๋ฐœ์ƒ", { error: err.message });

๋กœ๊ทธ ๋ ˆ๋ฒจ

const logger = getLogger(["myapp", "api"]);

logger.debug("๋””๋ฒ„๊ทธ ์ •๋ณด");     // ๊ฐœ๋ฐœ ์ค‘์—๋งŒ
logger.info("์ผ๋ฐ˜ ์ •๋ณด");        // ํ”„๋กœ๋•์…˜์—์„œ๋„
logger.warn("๊ฒฝ๊ณ ");            // ์ฃผ์˜ ํ•„์š”
logger.error("์—๋Ÿฌ");           // ์—๋Ÿฌ ๋ฐœ์ƒ
logger.fatal("์น˜๋ช…์  ์—๋Ÿฌ");     // ๋ณต๊ตฌ ๋ถˆ๊ฐ€๋Šฅํ•œ ์—๋Ÿฌ

๋กœ๊น… ์„ค์ •

sonamu.config.ts ์„ค์ •

export default {
  logging: {
    // Fastify ๋กœ๊น… ์นดํ…Œ๊ณ ๋ฆฌ (๊ธฐ๋ณธ๊ฐ’: ["fastify"])
    fastifyCategory: ["api", "request"],
    
    // ์ปค์Šคํ…€ Sink ์ถ”๊ฐ€
    sinks: {
      "file": getFileSink("logs/app.log")
    },
    
    // ์ปค์Šคํ…€ Filter ์ถ”๊ฐ€
    filters: {
      "production-only": (record) => 
        process.env.NODE_ENV === "production"
    },
    
    // Logger ์„ค์ •
    loggers: [
      {
        category: ["myapp"],
        sinks: ["console", "file"],
        lowestLevel: "info"
      }
    ]
  }
} satisfies SonamuConfig;

๋กœ๊น… ๋น„ํ™œ์„ฑํ™”

export default {
  logging: false  // ๋ชจ๋“  ๋กœ๊น… ๋น„ํ™œ์„ฑํ™”
} satisfies SonamuConfig;

์นดํ…Œ๊ณ ๋ฆฌ ๊ธฐ๋ฐ˜ ๋กœ๊น…

๊ณ„์ธต์  ์นดํ…Œ๊ณ ๋ฆฌ

// ์นดํ…Œ๊ณ ๋ฆฌ: ["myapp", "user", "auth"]
const authLogger = getLogger(["myapp", "user", "auth"]);

authLogger.info("๋กœ๊ทธ์ธ ์‹œ๋„", { email });
authLogger.error("์ธ์ฆ ์‹คํŒจ", { reason });

// ์นดํ…Œ๊ณ ๋ฆฌ: ["myapp", "order", "payment"]
const paymentLogger = getLogger(["myapp", "order", "payment"]);

paymentLogger.info("๊ฒฐ์ œ ์‹œ์ž‘", { orderId, amount });
paymentLogger.warn("๊ฒฐ์ œ ์ง€์—ฐ", { orderId });

Model/Frame๋ณ„ ์ž๋™ ์นดํ…Œ๊ณ ๋ฆฌ

Sonamu๋Š” Model๊ณผ Frame์— ์ž๋™์œผ๋กœ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ํ• ๋‹นํ•ฉ๋‹ˆ๋‹ค:
// UserModel -> ["sonamu", "model", "user"]
class UserModelClass extends BaseModel {
  async createUser(email: string) {
    // ์ž๋™ ๋กœ๊ฑฐ ์‚ฌ์šฉ
    this.logger.info("์‚ฌ์šฉ์ž ์ƒ์„ฑ", { email });
    return this.save({ email });
  }
}

// PaymentFrame -> ["sonamu", "frame", "payment"]
class PaymentFrameClass extends BaseFrame {
  async processPayment(amount: number) {
    this.logger.info("๊ฒฐ์ œ ์ฒ˜๋ฆฌ", { amount });
    // ...
  }
}

Fastify ๋กœ๊น…

API ์š”์ฒญ ๋กœ๊น…

Sonamu๋Š” ์ž๋™์œผ๋กœ /api ๊ฒฝ๋กœ์˜ ์š”์ฒญ์„ ๋กœ๊น…ํ•ฉ๋‹ˆ๋‹ค:
# ๋กœ๊ทธ ์ถœ๋ ฅ ์˜ˆ์‹œ
[api] [GET:200] /api/users?page=1 - Request completed
[api] [POST:201] /api/users - Request completed
[api] [PUT:400] /api/users/123 - Request completed

Healthcheck ์ œ์™ธ

/api/healthcheck๋Š” ์ž๋™์œผ๋กœ ๋กœ๊น…์—์„œ ์ œ์™ธ๋ฉ๋‹ˆ๋‹ค.

๋””๋ฒ„๊น… ํŒจํ„ด

ํ•จ์ˆ˜ ์ง„์ž…/์ข…๋ฃŒ ๋กœ๊น…

async function complexOperation(params: Params) {
  const logger = getLogger(["myapp", "operation"]);
  
  logger.debug("์ž‘์—… ์‹œ์ž‘", { params });
  
  try {
    const result = await doSomething(params);
    logger.debug("์ž‘์—… ์™„๋ฃŒ", { result });
    return result;
  } catch (error) {
    logger.error("์ž‘์—… ์‹คํŒจ", { error, params });
    throw error;
  }
}

์กฐ๊ฑด๋ถ€ ์ƒ์„ธ ๋กœ๊น…

const DEBUG = process.env.DEBUG === "true";

async function processData(data: Data[]) {
  const logger = getLogger(["myapp", "processor"]);
  
  logger.info("๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹œ์ž‘", { count: data.length });
  
  for (const item of data) {
    if (DEBUG) {
      logger.debug("ํ•ญ๋ชฉ ์ฒ˜๋ฆฌ ์ค‘", { item });
    }
    
    await processItem(item);
  }
  
  logger.info("๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์™„๋ฃŒ");
}

์—๋Ÿฌ ์ถ”์ 

async function apiCall() {
  const logger = getLogger(["myapp", "api"]);
  
  try {
    const response = await fetch(url);
    
    if (!response.ok) {
      logger.warn("API ์‘๋‹ต ์‹คํŒจ", { 
        status: response.status,
        url 
      });
    }
    
    return response.json();
  } catch (error) {
    logger.error("API ํ˜ธ์ถœ ์—๋Ÿฌ", { 
      error: error.message,
      stack: error.stack,
      url 
    });
    throw error;
  }
}

์ปค์Šคํ…€ Sink

ํŒŒ์ผ Sink

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

export default {
  logging: {
    sinks: {
      "error-file": getFileSink("logs/errors.log")
    },
    loggers: [
      {
        category: ["myapp"],
        sinks: ["error-file"],
        lowestLevel: "error"  // ์—๋Ÿฌ๋งŒ ํŒŒ์ผ์— ๊ธฐ๋ก
      }
    ]
  }
} satisfies SonamuConfig;

์ปค์Šคํ…€ Formatter

import { getConsoleSink } from "@logtape/logtape";
import { getPrettyFormatter } from "@logtape/pretty";

const customSink = getConsoleSink({
  formatter: getPrettyFormatter({
    timestamp: "date-time-timezone",  // ํƒ€์ž„์กด ํฌํ•จ
    categoryWidth: 30,                 // ์นดํ…Œ๊ณ ๋ฆฌ ๋„ˆ๋น„
    categoryTruncate: "end",          // ๋ง์ค„์ž„ ์œ„์น˜
    colors: true                       // ์ปฌ๋Ÿฌ ํ™œ์„ฑํ™”
  })
});

export default {
  logging: {
    sinks: {
      "custom-console": customSink
    }
  }
} satisfies SonamuConfig;

์ปค์Šคํ…€ Filter

ํ™˜๊ฒฝ๋ณ„ ํ•„ํ„ฐ๋ง

export default {
  logging: {
    filters: {
      "production-only": (record) => {
        return process.env.NODE_ENV === "production";
      },
      "no-debug-in-prod": (record) => {
        if (process.env.NODE_ENV === "production") {
          return record.level !== "debug";
        }
        return true;
      }
    },
    loggers: [
      {
        category: ["myapp"],
        filters: ["no-debug-in-prod"]
      }
    ]
  }
} satisfies SonamuConfig;

์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ๋ง

const apiOnlyFilter = (record: LogRecord) => {
  return record.category[0] === "api";
};

export default {
  logging: {
    filters: {
      "api-only": apiOnlyFilter
    },
    loggers: [
      {
        category: ["api"],
        filters: ["api-only"],
        sinks: ["api-file"]
      }
    ]
  }
} satisfies SonamuConfig;

๋กœ๊น… ๋ ˆ๋ฒจ ์ œ์–ด

๊ฐœ๋ฐœ/ํ”„๋กœ๋•์…˜ ๋ถ„๋ฆฌ

const isDev = process.env.NODE_ENV === "development";

export default {
  logging: {
    loggers: [
      {
        category: ["myapp"],
        lowestLevel: isDev ? "debug" : "info"  // ๊ฐœ๋ฐœ: debug, ํ”„๋กœ๋•์…˜: info
      }
    ]
  }
} satisfies SonamuConfig;

์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋ ˆ๋ฒจ

export default {
  logging: {
    loggers: [
      {
        category: ["myapp", "critical"],
        lowestLevel: "warn"  // ๊ฒฝ๊ณ  ์ด์ƒ๋งŒ
      },
      {
        category: ["myapp", "debug"],
        lowestLevel: "debug"  // ๋””๋ฒ„๊ทธ ํฌํ•จ
      },
      {
        category: ["myapp"],
        lowestLevel: "info"  // ๊ธฐ๋ณธ๊ฐ’
      }
    ]
  }
} satisfies SonamuConfig;

๊ตฌ์กฐํ™”๋œ ๋กœ๊น…

์ปจํ…์ŠคํŠธ ํฌํ•จ

const logger = getLogger(["myapp", "user"]);

logger.info("์‚ฌ์šฉ์ž ์ž‘์—…", {
  userId: user.id,
  action: "create",
  timestamp: new Date().toISOString(),
  metadata: {
    ip: request.ip,
    userAgent: request.headers["user-agent"]
  }
});

์—๋Ÿฌ ๊ฐ์ฒด ๋กœ๊น…

try {
  await riskyOperation();
} catch (error) {
  logger.error("์ž‘์—… ์‹คํŒจ", {
    error: {
      name: error.name,
      message: error.message,
      stack: error.stack,
      cause: error.cause
    }
  });
}

์„ฑ๋Šฅ ๋กœ๊น…

์‹คํ–‰ ์‹œ๊ฐ„ ์ธก์ •

async function measureOperation() {
  const logger = getLogger(["myapp", "perf"]);
  const start = performance.now();
  
  try {
    const result = await operation();
    const duration = performance.now() - start;
    
    logger.info("์ž‘์—… ์™„๋ฃŒ", { 
      duration: `${duration.toFixed(2)}ms`,
      result 
    });
    
    if (duration > 1000) {
      logger.warn("๋А๋ฆฐ ์ž‘์—… ๊ฐ์ง€", { duration });
    }
    
    return result;
  } catch (error) {
    const duration = performance.now() - start;
    logger.error("์ž‘์—… ์‹คํŒจ", { duration, error });
    throw error;
  }
}

๋กœ๊น… Best Practices

1. ์˜๋ฏธ ์žˆ๋Š” ๋ฉ”์‹œ์ง€

// โŒ ๋‚˜์œ ์˜ˆ
logger.info("done");
logger.error("error");

// โœ… ์ข‹์€ ์˜ˆ
logger.info("์‚ฌ์šฉ์ž ์ƒ์„ฑ ์™„๋ฃŒ", { userId, email });
logger.error("๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์‹คํŒจ", { 
  host, 
  error: err.message 
});

2. ์ ์ ˆํ•œ ๋กœ๊ทธ ๋ ˆ๋ฒจ

// โœ… ๋ ˆ๋ฒจ๋ณ„ ์ ์ ˆํ•œ ์‚ฌ์šฉ
logger.debug("๋ณ€์ˆ˜ ๊ฐ’ ํ™•์ธ", { value });           // ๊ฐœ๋ฐœ ์ค‘์—๋งŒ
logger.info("API ์š”์ฒญ ์™„๋ฃŒ", { statusCode });      // ์ผ๋ฐ˜ ์ •๋ณด
logger.warn("์žฌ์‹œ๋„ ํ•„์š”", { attempt, maxRetries }); // ์ฃผ์˜ ํ•„์š”
logger.error("์ฒ˜๋ฆฌ ์‹คํŒจ", { error });              // ์—๋Ÿฌ
logger.fatal("์„œ๋ฒ„ ์ข…๋ฃŒ", { reason });             // ์น˜๋ช…์ 

3. ๋ฏผ๊ฐ ์ •๋ณด ์ œ์™ธ

// โŒ ๋ฏผ๊ฐ ์ •๋ณด ๋กœ๊น…
logger.info("๋กœ๊ทธ์ธ", { 
  email,
  password  // โŒ
});

// โœ… ๋ฏผ๊ฐ ์ •๋ณด ๋งˆ์Šคํ‚น
logger.info("๋กœ๊ทธ์ธ", { 
  email,
  hasPassword: !!password  // โœ…
});

4. ์นดํ…Œ๊ณ ๋ฆฌ ์ผ๊ด€์„ฑ

// โœ… ๊ณ„์ธต์  ์นดํ…Œ๊ณ ๋ฆฌ ๊ตฌ์กฐ
["myapp", "user", "auth"]
["myapp", "user", "profile"]
["myapp", "order", "payment"]
["myapp", "order", "shipping"]

// โŒ ๋ถˆ๊ทœ์น™ํ•œ ์นดํ…Œ๊ณ ๋ฆฌ
["auth"]
["user-profile"]
["payments"]

๋””๋ฒ„๊น… ํŒ

1. ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๋ ˆ๋ฒจ ์ œ์–ด

# .env
LOG_LEVEL=debug
export default {
  logging: {
    loggers: [
      {
        category: ["myapp"],
        lowestLevel: process.env.LOG_LEVEL || "info"
      }
    ]
  }
} satisfies SonamuConfig;

2. ์กฐ๊ฑด๋ถ€ ์ƒ์„ธ ๋กœ๊น…

# ํŠน์ • ๊ธฐ๋Šฅ๋งŒ ๋””๋ฒ„๊ทธ
DEBUG=user,payment pnpm dev
const DEBUG_MODULES = process.env.DEBUG?.split(",") || [];

function shouldDebug(module: string) {
  return DEBUG_MODULES.includes(module);
}

if (shouldDebug("user")) {
  logger.debug("์ƒ์„ธ ์‚ฌ์šฉ์ž ์ •๋ณด", { user });
}

3. ๋กœ๊ทธ ๊ทธ๋ฃนํ™”

logger.info("=== ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ์‹œ์ž‘ ===", { orderId });
logger.info("1. ์žฌ๊ณ  ํ™•์ธ", { available });
logger.info("2. ๊ฒฐ์ œ ์ฒ˜๋ฆฌ", { amount });
logger.info("3. ๋ฐฐ์†ก ์ค€๋น„", { address });
logger.info("=== ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ===", { orderId });

๊ด€๋ จ ๋ฌธ์„œ