每個App都需要一個Logger
做一個可以在development和production環境都可以用到的logger

小弟仍然在網頁開發的學習道路上,因此本文並不算是教程,只是小弟在學習過程中發現到的覺得值得分享的編程做法。由於寫作水平有限,很多術語實在不知道怎麼翻譯,因此會有不少中英夾雜的情況,實在抱歉。如文中的語句或邏輯不清晰,請見諒。歡迎對文章分享您的看法以及聯絡交流,謝謝!
每個 Javascript 開發者在開發時都會用到 console.log 來查看 api 返回的結果或者函數的結果,尤其是在 debug 過程中,console.log 更是必不可少,可以最直觀地把 error 呈現在 broswer 或者 terminal 中。在 Development 環境中,我們並不介意 console.log 的結果直接顯示在 browser 中,但是在 Production 環境中則不一樣。一來是不希望讓用戶看到原始的 error 的信息(對用戶沒任何作用,APP 應該有 error-handling 的機制呈現恰當的錯誤信息給用戶),二來是希望 這些 error 信息可以被如 Sentry 這類專門檢測網站或者原生 APP(下文統稱 APP) 的表現及報錯的軟件可以捕抓並記錄下來,方便開發者之後進行修補。這時候就需要一個 Logger 承擔起 APP 的檢測任務,能夠把所有在 APP 發生的事件包括 error 都捕抓並在合適的時候顯示出來。
接下來會寫一個Logger Class專門處理在不同運行環境中,不同重要程度的 log。這些重要程度包括:debug, info, log, warn, error。
例子會用 next.js 作為網站的 framework,連接 sentry 實現 Production 環境的報錯。
引入需要的 dependency
date-fns/format用作處理時間數據。
import * as Sentry from "@sentry/nextjs";
import format from "date-fns/format";定義 Log 的重要程度的 Type
這裡用enum而不是直接用 type 是為了在 runtime 時都可以直接調用LogLevel,降低編程 typo 的出錯機會。
export enum LogLevel {
Debug = 'debug',
Info = 'info',
Log = 'log',
Warn = 'warn',
Error = 'error',
}定義發送到Sentry的 Metadata 的 Type
type Metadata = {
/**
* 用到Sentry breadcrumb types, 預設是 `default`. 詳情可以到下面鏈接:
* @see https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types
*/
type?:
| "default"
| "debug"
| "error"
| "navigation"
| "http"
| "info"
| "query"
| "transaction"
| "ui"
| "user",
/**
* tags會放到Sentry.captureException的函數裡面,詳情可以看下面鏈接
*
* @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65
*/
tags?: {
[key: string]:
| number
| string
| boolean
| bigint
| symbol
| null
| undefined,
},
/**
* 任何其它額外的數據都可以通過 extra 這個參數放在Sentry.captureException的函數裡面,在之後debug時使用。
*/
[key: string]: unknown,
} & Parameters<typeof Sentry.captureException>[1];定義Transport的 Type
Logger 在 Development 環境會用 console.log,而在 Production 環境會通過 sentry/nextjs (Sentry SDK)發送到 Sentry,不需要使用 console.log。因此會寫兩個 Transport,分別是consoleTransport 和 sentryTransport。
基本 Transport Type
type Transport = (
level: LogLevel,
message: string | Error,
metadata: Metadata,
timestamp: number
) => void;consoleTransport Type
export const consoleTransport: Transport = (
level,
message,
metadata,
timestamp
) => {
const extra = Object.keys(metadata).length
? " " + JSON.stringify(metadata, null, " ")
: "";
const log = {
[LogLevel.Debug]: console.debug,
[LogLevel.Info]: console.info,
[LogLevel.Log]: console.log,
[LogLevel.Warn]: console.warn,
[LogLevel.Error]: console.error,
}[level];
log(`${format(timestamp, "HH:mm:ss")} ${message.toString()}${extra}`);
};sentryTransport Type
export const sentryTransport: Transport = (
level,
message,
{ type, tags, ...metadata },
timestamp,
) => {
if (typeof message === 'string') {
const severity = (
{
[LogLevel.Debug]: 'debug',
[LogLevel.Info]: 'info',
[LogLevel.Log]: 'log',
[LogLevel.Warn]: 'warning',
[LogLevel.Error]: 'error',
} as const
)[level];
Sentry.addBreadcrumb({
message,
data: metadata,
type: type || 'default',
level: severity,
timestamp: timestamp / 1000, // Sentry expects seconds
});
if (level === 'error' || level === 'warn' || level === 'log') {
const messageLevel = ({
[LogLevel.Log]: 'log',
[LogLevel.Warn]: 'warning',
[LogLevel.Error]: 'error',
}[level] || 'log') as Sentry.Breadcrumb['level'];
Sentry.captureMessage(message, {
level: messageLevel,
tags,
extra: metadata,
});
}
} else {
/**
* It's otherwise an Error and should be reported with captureException
*/
Sentry.captureException(message, {
tags,
extra: metadata,
});
}
};最後兩個前置動作
創建 debugContext.ts 文件
為在本地 debug 時更好地對 error message 進行分類,加入DebugContext,在DebugContext object 中加任意的資料,例如想標明 error 來自 session 組件的,便加入 session: 'session' 。DebugContext稍後會被引入至logger.ts使用。
/**
* 當在APP的其它地方,注意不要直接從這個文件引入,而是用`logger.DebugContext`
*/
export const DebugContext = {
// e.g. composer: 'composer'
session: 'session',
notifications: 'notifications',
} as const創建 Logger Class
到了打 Boss 的時候了,接下來寫 Logger Class
export class Logger {
LogLevel = LogLevel;
DebugContext = DebugContext;
enabled: boolean;
level: LogLevel;
transports: Transport[] = [];
protected debugContextRegexes: RegExp[] = [];
constructor({
enabled = process.env.NODE_ENV !== 'test',
level = process.env.NEXT_PUBLIC_LOG_LEVEL as LogLevel,
debug = '',
}: {
enabled?: boolean;
level?: LogLevel;
debug?: string;
} = {}) {
this.enabled = enabled !== false;
this.level = debug ? LogLevel.Debug : level ?? LogLevel.Info; //default to info
this.debugContextRegexes = (debug || '').split(',').map(context => {
return new RegExp(context.replace(/[^\w:*]/, '').replace(/\*/g, '.*'));
});
}
debug(message: string, metadata: Metadata = {}, context?: string) {
if (context && !this.debugContextRegexes.find(reg => reg.test(context))) {
return;
}
this.transport(LogLevel.Debug, message, metadata);
}
info(message: string, metadata: Metadata = {}) {
this.transport(LogLevel.Info, message, metadata);
}
log(message: string, metadata: Metadata = {}) {
this.transport(LogLevel.Log, message, metadata);
}
warn(message: string, metadata: Metadata = {}) {
this.transport(LogLevel.Warn, message, metadata);
}
error(error: string | Error, metadata: Metadata = {}) {
this.transport(LogLevel.Error, error, metadata);
}
addTransport(transport: Transport) {
this.transports.push(transport);
return () => {
this.transports.splice(this.transports.indexOf(transport));
};
}
disable() {
this.enabled = false;
}
enable() {
this.enabled = true;
}
protected transport(
level: LogLevel,
message: string | Error,
metadata: Metadata = {},
) {
if (!this.enabled) {
return;
}
if (!enabledLogLevels[this.level].includes(level)) {
return;
}
const timestamp = Date.now();
const meta = metadata || {};
for (const transport of this.transports) {
transport(level, message, meta, timestamp);
}
add({
id: nanoid(),
timestamp,
level,
message,
metadata: meta,
});
}
}
export const logger = new Logger()最後設定 logger 在不同環境中放入不同的 Transport
在 Development 環境用 console, Production 用 Sentry, Test 不做任何 log
if (env.IS_DEV && !env.IS_TEST) {
logger.addTransport(consoleTransport);
} else if (env.IS_PROD) {
logger.addTransport(sentryTransport);
}From the blog
The other posts of this topic.

建立一個 Modal 狀態控制中心
告別散亂的 Modals,用 React Context 管理 Application 中所有 Modal
20 Nov 2023
Deploying Next.js Apps
How to deploy your Next.js apps on Vercel.
02 Jan 2023
Dynamic Routing and Static Regeneration
How to use incremental static regeneration using dynamic routes.
04 Mar 2023