Next.js 如何增加 ELK Log 功能


為什麼要在 Next.js 中增加 ELK Log 功能?

在現代的 web 應用開發中,追蹤和記錄應用程式的活動是非常重要的一環。這不僅有助於除錯,還能讓我們更了解應用程式的運行狀況,提升用戶體驗。ELK Stack(Elasticsearch, Logstash, Kibana)是一個強大的日誌管理工具,能夠幫助我們收集、處理和展示日誌數據。那麼,該如何在 Next.js 專案中增加 ELK Log 功能呢?讓我們一步步來了解。

ELK Stack 簡介

在進入實作之前,讓我們先來快速了解一下 ELK Stack 的組成部分:

  • Elasticsearch:一個強大的搜索引擎,用於儲存和搜索日誌數據。
  • Logstash:用於收集和處理日誌數據,並將其傳送至 Elasticsearch。
  • Kibana:一個視覺化工具,用於展示和分析 Elasticsearch 中的數據。

這三個工具結合在一起,提供了一個完整的日誌管理解決方案。

在 Next.js 中增加 ELK Log 功能的步驟

1. 安裝所需的套件

首先,我們需要安裝一些 Node.js 套件來幫助我們實現日誌功能。這裡我們會使用 winston 來記錄日誌,並使用 node-fetch 將日誌傳送至 ELK。

npm install winston node-fetch

2. 設定 Logger

接下來,我們需要設定一個 Logger。這個 Logger 會負責記錄日誌並將其傳送至 ELK。

伺服器端 Logger 設定

// lib/serverLogger.ts
import fs from 'fs';
import path from 'path';
import fetch from 'node-fetch';
import winston from 'winston';

const { combine, timestamp, printf } = winston.format;

const logFormat = printf(({ level, message, timestamp }) => {
  return `${timestamp} ${level}: ${message}`;
});

const logger = winston.createLogger({
  level: 'info',
  format: combine(
    timestamp(),
    logFormat
  ),
  transports: [
    new winston.transports.File({ filename: 'logs/app.log', level: 'info' }),
    new winston.transports.Console()
  ],
});

const elkUrl = process.env.ELK_URL || null;

if (elkUrl) {
  logger.add(new winston.transports.Http({
    level: 'info',
    format: winston.format.json(),
    host: new URL(elkUrl).hostname,
    path: new URL(elkUrl).pathname,
  }));
}

export default logger;

客戶端 Logger 設定

// lib/clientLogger.ts
import winston from 'winston';

const { combine, timestamp, printf } = winston.format;

const logFormat = printf(({ level, message, timestamp }) => {
  return `${timestamp} ${level}: ${message}`;
});

const logger = winston.createLogger({
  level: 'info',
  format: combine(
    timestamp(),
    logFormat
  ),
  transports: [
    new winston.transports.Console()
  ],
});

const elkUrl = process.env.NEXT_PUBLIC_ELK_URL || null;

if (elkUrl) {
  logger.add(new winston.transports.Http({
    level: 'info',
    format: winston.format.json(),
    host: new URL(elkUrl).hostname,
    path: new URL(elkUrl).pathname,
  }));
}

export default logger;

3. 在 Middleware 中使用 Logger

在 Next.js 中,我們可以使用 Middleware 來攔截請求,並在這裡使用 Logger 來記錄請求日誌。

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';
import logger from '@/lib/serverLogger';

const secret = process.env.NEXTAUTH_SECRET;

export default async function middleware(req: NextRequest) {
  logger.info(`Request received: ${req.url}`);

  const token = await getToken({ req, secret });

  if (token) {
    logger.info(`User authenticated: ${token.email}`);
  } else {
    logger.warn('Unauthenticated request');
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/api/:path*'],
};

4. 在 API 路由中使用 Logger

我們也可以在 API 路由中使用 Logger 來記錄各種操作。例如:

// pages/api/user.ts
import { NextApiRequest, NextApiResponse } from 'next';
import logger from '@/lib/serverLogger';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  logger.info('User API endpoint hit');

  if (req.method === 'GET') {
    logger.info('Fetching user data');
    res.status(200).json({ name: 'John Doe' });
  } else {
    logger.warn('Unsupported request method');
    res.status(405).end();
  }
}

5. 在客戶端使用 Logger

客戶端的日誌可以用來記錄用戶行為及其他前端事件。例如:

// pages/index.tsx
import React, { useEffect } from 'react';
import logger from '@/lib/clientLogger';

const HomePage = () => {
  useEffect(() => {
    logger.info('HomePage component mounted');
  }, []);

  const handleClick = () => {
    logger.info('Button clicked');
  };

  return (
    <div>
      <h1>Welcome to Next.js with ELK Logging!</h1>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
};

export default HomePage;

6. 視覺化日誌數據

完成上述步驟後,我們可以在 Kibana 中配置並視覺化我們的日誌數據。這將有助於我們更直觀地了解應用程式的運行狀況。

完整實作範例

以下是一個完整的實作範例,包含伺服器端和客戶端的日誌設定、Middleware 和 API 路由中的日誌記錄:

伺服器端日誌設定

// lib/serverLogger.ts
import fs from 'fs';
import path from 'path';
import fetch from 'node-fetch';
import winston from 'winston';

const { combine, timestamp, printf } = winston.format;

const logFormat = printf(({ level, message, timestamp }) => {
  return `${timestamp} ${level}: ${message}`;
});

const logger = winston.createLogger({
  level: 'info',
  format: combine(
    timestamp(),
    logFormat
  ),
  transports: [
    new winston.transports.File({ filename: 'logs/app.log', level: 'info' }),
    new winston.transports.Console()
  ],
});

const elkUrl = process.env.ELK_URL || null;

if (elkUrl) {
  logger.add(new winston.transports.Http({
    level: 'info',
    format: winston.format.json(),
    host: new URL(elkUrl).hostname,
    path: new URL(elkUrl).pathname,
  }));
}

export default logger;

客戶端日誌設定

// lib/clientLogger.ts
import winston from 'winston';

const { combine, timestamp, printf } = winston.format;

const logFormat = printf(({ level, message, timestamp }) => {
  return `${timestamp} ${level}: ${message}`;
});

const logger = winston.createLogger({
  level: 'info',
  format: combine(
    timestamp(),
    logFormat
  ),
  transports: [
    new winston.transports.Console()
  ],
});

const elkUrl = process.env.NEXT_PUBLIC_ELK_URL || null;

if (elkUrl) {
  logger.add(new winston.transports.Http({
    level: 'info',
    format: winston.format.json(),
    host: new URL(elkUrl).hostname,
    path: new URL(elkUrl).pathname,
  }));
}

export default logger;

Middleware 日誌記錄

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';
import logger from '@/lib/serverLogger';

const secret = process.env.NEXTAUTH_SECRET;

export default async function

 middleware(req: NextRequest) {
  logger.info(`Request received: ${req.url}`);

  const token = await getToken({ req, secret });

  if (token) {
    logger.info(`User authenticated: ${token.email}`);
  } else {
    logger.warn('Unauthenticated request');
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/api/:path*'],
};

API 路由日誌記錄

// pages/api/user.ts
import { NextApiRequest, NextApiResponse } from 'next';
import logger from '@/lib/serverLogger';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  logger.info('User API endpoint hit');

  if (req.method === 'GET') {
    logger.info('Fetching user data');
    res.status(200).json({ name: 'John Doe' });
  } else {
    logger.warn('Unsupported request method');
    res.status(405).end();
  }
}

客戶端日誌記錄

// pages/index.tsx
import React, { useEffect } from 'react';
import logger from '@/lib/clientLogger';

const HomePage = () => {
  useEffect(() => {
    logger.info('HomePage component mounted');
  }, []);

  const handleClick = () => {
    logger.info('Button clicked');
  };

  return (
    <div>
      <h1>Welcome to Next.js with ELK Logging!</h1>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
};

export default HomePage;

結論

在 Next.js 專案中增加 ELK Log 功能,可以大幅提升應用程式的可觀察性和除錯效率。通過 ELK Stack,我們可以輕鬆收集、處理和視覺化日誌數據,讓開發和運維工作更加高效。如果你還沒有在專案中使用 ELK Stack,現在就開始吧,讓你的應用程式變得更健壯!


希望這篇文章能夠幫助你在 Next.js 專案中順利增加 ELK Log 功能,並為你的開發工作帶來更多便利。如果你有任何問題或建議,歡迎在下方留言與我們分享。Happy coding!