Closed7

Next.jsのPageRouterでNewRelicにログを送る

Yutaka FujiiYutaka Fujii

Next.jsでNewRelicを使う場合、超えないといけない壁がやたら多い印象がある。

Next.jsには3つの実行環境がある。

  • ブラウザランタイム
    • Webブラウザで動作する
  • サーバランタイム
    • Node.jsサーバで動作する
  • ミドルウェアランタイム
    • Edgeランタイムで動作する(≠Node.jsサーバじゃない)

NewRelicはブラウザ側とサーバ側でそれぞれ対応する内容が違う。

  • ブラウザはBrowserAgentで動作
    • 極論、スニペットを埋め込んでwindow.newrelic.log()とかで送ることができる。
  • サーバはnewrelic.jsで動作
    • pinoなどのロガーをapplication_loggingからforwardingできる。
    • apiルートなどで動作
  • ミドルウェアは...結論からいうとミドルウェアでは動作できない
    • ミドルウェアでは発火できないため、fetchでapiルートにリクエストを送り、apiルートからnewrelicへログを転送させる
  • 上記いずれもNewRelicの公式ドキュメントのどこに何が書いてあるのか、結局よくわからなかった。。。誰か詳しい見方を教えてください。
Yutaka FujiiYutaka Fujii

Next.jsにNewRelicのサーバ側設定を行う。

  • newrelicで無料アカウントを作成
  • 管理コンソールを初回開いた際には色々と選択させられるが適当で良い
    • 自分のPCをホスト登録するのは監視されてる感じがするのでおすすめしない
  • 適当に設定したら自分のアカウントからAPI Keyを作成する
    • Create an API key -> key type Ingest - License
  • envファイルにライセンスキー、アプリ名を記載
# newrelicの設定ファイルのあるディレクトリを指定
NEW_RELIC_HOME=o11y
# newrelicの起動オプションの設定
NODE_OPTIONS='-r @newrelic/next'
# newrelicのアプリケーション名を指定(監視対象のアプリケーション名になる)
NEW_RELIC_APP_NAME=
# newrelicに登録したライセンスキーを指定(漏洩不可)
# newrelic.jsに環境変数を読ませるためにはnode-env-runを使う必要がある
NEW_RELIC_LICENSE_KEY=

必要なライブラリを導入

npm i newrelic @newrelic/next pino pino-pretty
npm i -D node-env-run @types/newrelic

newrelic.jsを設定

  • 雛形はnode_modules/newrelic/newrelic.jsらしい
  • 下記のようにカスタマイズする
"use strict";

// 参考設定
// https://www.thisdot.co/blog/integrating-next-js-with-new-relic

const app_name = process.env.NEW_RELIC_APP_NAME;
const license_key = process.env.NEW_RELIC_LICENSE_KEY;

exports.config = {
  app_name: [app_name],
  license_key,
  // https://docs.newrelic.com/jp/docs/logs/logs-context/configure-logs-context-nodejs/
  application_logging: {
    forwarding: {
      enabled: true,
    },
  },
  // ロギングの設定により標準出力をトレースする
  logging: {
    level: "info",
    enabled: true, // ログの収集を有効にする
  },
  allow_all_headers: true,
  attributes: {
    /**
     * Prefix of attributes to exclude from all destinations. Allows * as wildcard
     * at end.
     *
     * NOTE: If excluding headers, they must be in camelCase form to be filtered.
     *
     * @name NEW_RELIC_ATTRIBUTES_EXCLUDE
     */
    exclude: [
      "request.headers.cookie",
      "request.headers.authorization",
      "request.headers.proxyAuthorization",
      "request.headers.setCookie*",
      "request.headers.x*",
      "response.headers.cookie",
      "response.headers.authorization",
      "response.headers.proxyAuthorization",
      "response.headers.setCookie*",
      "response.headers.x*",
    ],
  },
};

npm scriptを下記のように修正

  • node-env-runでないとnewrelic.jsに環境変数を読み取らせることができない。
  • 他にも使えるものはあるかもしれないが、だめだった。
"dev": "nodenv -E .env --exec \"next dev\"",

pinoを設定する

import pino from "pino";

/**
 * NewRelicでログ情報をキャッチするログを出力する
 */
export const apiLogger = pino({
  level: process.env.LOG_LEVEL || "info",
  base: {
    logtype: "application", // ログタイプを指定
  },
  transport: {
    target: "pino-pretty",
    options: {
      colorize: true, // 色を付けるオプション
    },
  },
});

Loggerをapiルートに設定する

  • src/pages/api/hello.api.ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import {apiLogger} from "@/libs/apiLogger";
import type {NextApiRequest, NextApiResponse} from "next";

type Data = {
  name: string;
};

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  apiLogger.info("Hello API For Pino");
  res.status(200).json({name: "John Doe"});
}

起動 -> アクセス

  • http://localhost:3000/api/helloでアクセスする
  • NewRelicの管理コーンソールからQueryYourDataにて SELECT * FROM Log と入力すると"Hello API For Pino"が出力されている(かもしれない)
  • All Entityにはenvに設定したNEW_RELIC_APP_NAMEが設定されているはず
Yutaka FujiiYutaka Fujii

Next.jsにNewRelicのブラウザ側設定を行う

  • ライブラリnewrelicからBrowserAgentを設定したい気持ちはわかるが、上手くいかないので、newrelic.getBrowserTimingHeaderを用いる。
  • src/_document.page.tsxにnewrelic.getBrowserTimingHeaderをgetInitialPropsで生成する。
  • なお、関数型で定義すると無限ループになり、防ぐことができなかったためクラス書式で記述してる。
import Document, {
  Html,
  Head,
  Main,
  NextScript,
  DocumentContext,
  DocumentInitialProps,
} from "next/document";
import newrelic from "newrelic";

type MyDocumentProps = DocumentInitialProps & {
  browserTimingHeader: string;
};

class MyDocument extends Document<MyDocumentProps> {
  static async getInitialProps(
    ctx: DocumentContext
  ): Promise<DocumentInitialProps & {browserTimingHeader: string}> {
    const initialProps = await Document.getInitialProps(ctx);

    // NewRelicのスクリプトを生成
    const browserTimingHeader = newrelic.getBrowserTimingHeader({
      hasToRemoveScriptWrapper: true,
    });

    return {
      ...initialProps,
      browserTimingHeader,
    };
  }

  render() {
    return (
      <Html lang="en">
        <Head />
        <script
          type="text/javascript"
          dangerouslySetInnerHTML={{__html: this.props.browserTimingHeader}}
        />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}
export default MyDocument;
  • これを行うと、scriptタグにNewRelicのスニペットが生成される。
  • おそらくnewrelic.js経由でライセンスキーが読み取られ、それに応じたapplicationIdなどの必要情報が設定されるものだと思われる。
  • NewRelicのスニペットが発火するとwindowオブジェクトにnewrelicが生えるため、window.newrelic.log()など使用できるようになる。
  • windowオブジェクトを型補完するためには下記の設定を行う。合ってるかはわからないけど、使う面では不足ない。
import {BrowserAgent} from "@newrelic/browser-agent";

/**
 * NewRelicに必要な型定義を追加
 */
declare global {
  interface Window {
    NREUM?: object | boolean;
    // NewRelicに情報を追加する関数などが追加されている
    newrelic?: BrowserAgent;
  }
}
/**
 * newrelicに直接ログを送信する
 * @param message
 * @param options
 * @MEMO QueryYourData -> SELECT * FROM Log にて確認できる
 */
export const nrLog = (
  message: string,
  options?: {
    customAttributes?: object;
    level?: "ERROR" | "TRACE" | "DEBUG" | "INFO" | "WARN";
  }
) => {
  window.newrelic?.log(message, options);
};
  • nrLog("send log")みたいに使う
/**
 * newrelicのページアクショレポートを送信する
 * @param name
 * @param attributes
 * @MEMO QueryYourData -> SELECT * FROM PageAction にて確認できる
 */
export const nrPageAction = (name: string, attributes?: object | undefined) => {
  /**
   * NewRelicのBrowserAgentはNext.jsと相性が良くない。
   * SSR時にNREUMが存在しないためエラーになる。
   * これを防ぐためにはnode_modules内のnewrelicに修正が必要になるため、
   * windowオブジェクト内に生成されるnewrelicを利用する。
   */
  window.newrelic?.addPageAction(name, attributes);
};
  • nrPageAction("send page action")みたいに使う
Yutaka FujiiYutaka Fujii

Next.jsにNewRelicのミドルウェア設定を行う

  • ここまでで、サーバ側、ブラウザ側でのログ出力は一旦できるようになった。
  • ブラウザ側でpinoを使う設定をする。
    • pinoでログ出力し、apiルートに仕込んだログを送る仕組みと同期させる。
  • ミドルウェアではブラウザ設定を組み込んだpinoは発火するが、newrelic.jsのログフォワーディングは適用されないようなので、ブラウザと同じように組み込む。

ブラウザ側で使用するpinoの設定

import pino from "pino";

/**
 * ブラウザ+ミドルウェア環境で発生したログ情報をapi/logsに送信する
 * @MEMO https://qiita.com/P-man_Brown/items/0f0e0613fd9bb3e8c99c
 */
export const browerLogger = pino({
  level: process.env.LOG_LEVEL || "info", // 出力するログレベルを設定。指定したレベル以上のログが出力される
  timestamp: pino.stdTimeFunctions.isoTime, // タイムスタンプの形式の設定
  browser: {
    write: () => {}, // ブラウザのコンソールにログを出力しないようにする
    // ブラウザやミドルウェアのログをサーバーに送信するための設定
    transmit: {
      send: (level, logEvent) => {
        // childを使用する場合にはlogEvent.messagesだけでなく、bindingsもサーバーに送信する必要がある
        const messages = logEvent.messages;
        // /api/logにリクエストを送る
        fetch("http://localhost:3000/api/logs", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({level, messages}),
          keepalive: true,
        });
      },
    },
  },
});

src/api/logs.api.ts

  • POSTで受け取ることができるようにしておく。
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import {apiLogger} from "@/libs/apiLogger";
import type {NextApiRequest, NextApiResponse} from "next";

type Data = {
  name: string;
};

/**
 * NewRelicにログを収集するためのAPI
 * @param req
 * @param res
 */
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  if (req.method === "POST") {
    const {level = "info", messages} = (await req.body) as {
      level: "info";
      messages: string;
    };
    apiLogger[level](messages);
  }

  res.status(200).json({name: "Log API"});
}

middleware.page.ts

  • 人によってmiddlewareの置き場が異なる模様(ちゃんと調べてない)
  • 自分は src/middleware.ts に配置して動作した
import {NextRequest, NextResponse} from "next/server";
import {browerLogger} from "./libs/browserLogger";

/**
 * @MEMO middlewareはEdgeランタイムで実行されるため、
 * middleware内では使用できるモジュールに制限がある。
 */
export function middleware() {
  // middleware内でロガーを設定しても上手く動作しないため、api/logsにログ情報を送信する
  browerLogger.info("middleware Pino");
  return NextResponse.next();
}

// ミドルウェアを適用するパスを指定
export const config = {
  matcher: [
    // ログを送信するパス以外のリクエストにミドルウェアを適用
    "/(!api/logs)",
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico, sitemap.xml, robots.txt (metadata files)
     */
    "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
  ],
};
  • これでリクエストの度、middlewareを通過するのでログが記録されるようになる。
Yutaka FujiiYutaka Fujii

EX.NewRelicAgent 経由での設定例

  • この設定だけだとうまく動作しないところがある。
  • 動作しているところもある。全般的に謎。

newrelic.js の設定例

"use strict";

// 参考設定
// https://www.thisdot.co/blog/integrating-next-js-with-new-relic

const app_name = process.env.NEW_RELIC_APP_NAME;
const license_key = process.env.NEW_RELIC_LICENSE_KEY;

exports.config = {
  app_name: [app_name],
  license_key,
  // https://docs.newrelic.com/jp/docs/logs/logs-context/configure-logs-context-nodejs/
  application_logging: {
    forwarding: {
      enabled: true,
    },
  },
  // ロギングの設定により標準出力をトレースする
  logging: {
    level: "info",
    enabled: true, // ログの収集を有効にする
  },
  allow_all_headers: true,
  attributes: {
    /**
     * Prefix of attributes to exclude from all destinations. Allows * as wildcard
     * at end.
     *
     * NOTE: If excluding headers, they must be in camelCase form to be filtered.
     *
     * @name NEW_RELIC_ATTRIBUTES_EXCLUDE
     */
    exclude: [
      "request.headers.cookie",
      "request.headers.authorization",
      "request.headers.proxyAuthorization",
      "request.headers.setCookie*",
      "request.headers.x*",
      "response.headers.cookie",
      "response.headers.authorization",
      "response.headers.proxyAuthorization",
      "response.headers.setCookie*",
      "response.headers.x*",
    ],
  },
};

NewRelicAgent の設定例

import {BrowserAgent, GenericEvents} from "@newrelic/browser-agent";
import {useEffect} from "react";

/**
 * NewRelicのブラウザから設定情報を取得
 * Browser-> Application -> ApplicationSetting -> AgentAndAccountのJavaScriptSnipetから取得
 */
export const options = {
  init: {
    distributed_tracing: {enabled: true},
    privacy: {cookies_enabled: true},
    ajax: {deny_list: ["bam.nr-data.net"]},
  },
  loader_config: {
    accountID: process.env.NEXT_PUBLIC_NEW_RELIC_ACCOUNT_ID,
    trustKey: process.env.NEXT_PUBLIC_NEW_RELIC_TRUST_KEY,
    agentID: process.env.NEXT_PUBLIC_NEW_RELIC_AGENT_ID,
    licenseKey: process.env.NEXT_PUBLIC_NEW_RELIC_LICENSE_KEY,
    applicationID: process.env.NEXT_PUBLIC_NEW_RELIC_APPLICATION_ID,
  },
  info: {
    beacon: "bam.nr-data.net",
    errorBeacon: "bam.nr-data.net",
    licenseKey: process.env.NEXT_PUBLIC_NEW_RELIC_LICENSE_KEY,
    applicationID: process.env.NEXT_PUBLIC_NEW_RELIC_APPLICATION_ID,
    sa: 1,
  },
  features: [GenericEvents],
};

/**
 * newrelic.getBrowserTimingHeaderのみで動作する模様
 * 本当はトレードオフになる設定ではないかと思う
 */
const NewRelicAgent = () => {
  useEffect(() => {
    // https://github.com/newrelic/newrelic-browser-agent/issues/865#issuecomment-2116600043
    if (typeof window !== "undefined") {
      const agent = new BrowserAgent(options);
      agent.start();
    }
  }, []);

  return null;
};

export default NewRelicAgent;

src/pages/_app.page.tsx の設定例

  • NewRelicAgent を Next.js 経由で追加する場合、SSR で発火しないように工夫する必要がある。
import "@/styles/globals.css";
import type {AppProps} from "next/app";
import dynamic from "next/dynamic";

const NewRelicAgent = dynamic(() => import("../components/NewRelicAgent"), {
  ssr: false,
});

export default function App({Component, pageProps}: AppProps) {
  return (
    <>
      <NewRelicAgent />
      <Component {...pageProps} />
    </>
  );
}

next.config.js の設定例

  • nrExternals は、おそらく BrowserAgent を使う場合に必要になる。
  • Next.js は BrowserAgent を利用する際に SSR 側で動作不良があるため、BrowserAgent を利用しないほうが安定する。
import nrExternals from "@newrelic/next/load-externals.js";

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  pageExtensions: ["page.tsx", "page.ts", "api.ts"],
  webpack: (config) => {
    nrExternals(config);
    return config;
  },
};

export default nextConfig;
Yutaka FujiiYutaka Fujii

いろいろエラーにも遭遇して、それらの解決方法についての知見もあるが、気力がなくなったのでまた今度。。。

このスクラップは2024/10/16にクローズされました