axios でリクエストログを出力する

JavaScriptでXHRやFetchなどで通信するとブラウザの開発コンソールのネットワークなどにログが出力されますが、Next.js のSSRなど、Node.js で実行する場合はログはでないです。そのため、通信ログが欲しい場合は、なんらかの方法でログを出力する必要がありますが、axios を使用している場合、以下のようにすると通信ログが出力されるようです。

import axios from 'axios';

axios.interceptors.request.use((request) => {
  console.log('Starting Request: ', request);
  return request;
});

axios.interceptors.response.use((response) => {
  console.log('Response: ', response);
  return response;
});

axios.get("https://example.com");

簡易的な確認の場合、これでもいいのですが少々見にくいので以下のように整形すると見やすいです。

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';

/**
 * JSONを整形する場合は`true`にする
 */
const pretty = false;

/**
 * `axios`で通信するときにログを出力する
 * @notice 秘匿情報のマスクなどは行っていないので、ローカル環境でのみ有効にするかマスク処理を追加する。
 */
export function injectAxiosLogger(instance: AxiosInstance): AxiosInstance {
  if (process.env.NODE_ENV === 'production') return instance;
  const bar = [...times(120)].map((_) => '-').join('');

  instance.interceptors.request.use((request) => {
    const req = dumpAxiosRequestConfig(request);
    console.log(`${bar}\nStarting HTTP Request\n${bar}\n${req}\n${bar}`);
    return request;
  });

  instance.interceptors.response.use((response) => {
    const req = dumpAxiosRequestConfig(response.config);
    const res = dumpAxiosResponse(response);
    console.log(
      `${bar}\nHTTP Request Completed\n${bar}\n${req}\n${bar}\n${res}\n${bar}`
    );
    return response;
  });

  return instance;
}

/**
 * HTTPリクエストをダンプする
 * @see AxiosRequestConfig https://github.com/axios/axios?tab=readme-ov-file#request-config
 * @see https://sabljakovich.medium.com/axios-interceptors-log-request-and-response-72b01333a760
 */
export function dumpAxiosRequestConfig(req: AxiosRequestConfig) {
  const method = req.method?.toLocaleLowerCase() || '';
  const baseURL = req.baseURL || '';
  const path = req.url || '';
  const params =
    req.params instanceof URLSearchParams
      ? req.params
      : new URLSearchParams(req.params || {});
  let headers = req.headers || {};
  headers = {
    ...(headers.common || {}),
    ...(headers[method] || {}),
    ...headers,
  };
  for (let h of ['common', 'get', 'post', 'head', 'put', 'patch', 'delete']) {
    delete headers[h];
  }
  const data = req.data;

  const rawParams = params.toString();
  const rawHeaders = Object.entries(headers)
    .map(([k, v]) => `${k}: ${v}`)
    .join('\n');
  const rawData = dumpData(data);
  let msg = `${method.toUpperCase()} ${baseURL}${path} HTTP/1.1`;
  msg += rawParams ? `?${rawParams}` : '';
  msg += rawHeaders ? `\n${rawHeaders}` : '';
  msg += rawData ? `\n\n${rawData}` : '';
  return msg;
}

/**
 * HTTPレスポンスをダンプする
 * @see AxiosResponse https://github.com/axios/axios?tab=readme-ov-file#response-schema
 */
export function dumpAxiosResponse<T>(res: AxiosResponse<T>) {
  const status = res.status;
  const statusText = res.statusText;
  const headers = res.headers || {};
  const data = res.data;

  const rawHeaders = Object.entries(headers)
    .map(([k, v]) => `${k}: ${v}`)
    .join('\n');
  const rawData = dumpData(data);

  let msg = `HTTP/1.1 ${status} ${statusText}`;
  msg += rawHeaders ? `\n${rawHeaders}` : '';
  msg += rawData ? `\n\n${rawData}` : '';
  return msg;
}

function *times(n: number) {
  for (let i = 0; i < n; i++) yield i;
}

function dumpData(data: any) {
  return data
    ? typeof data === 'object'
      ? pretty
        ? JSON.stringify(data, null, ' ')
        : JSON.stringify(data)
      : data
    : '';
}

実行結果:

------------------------------------------------------------------------------------------------------------------------
Starting HTTP Request
------------------------------------------------------------------------------------------------------------------------
GET https://example.com HTTP/1.1
Accept: application/json, text/plain, */*
Content-Type: undefined
------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------
HTTP Request Completed
------------------------------------------------------------------------------------------------------------------------
GET https://example.com HTTP/1.1
Accept: application/json, text/plain, */*
Content-Type: undefined
User-Agent: axios/1.6.0
Accept-Encoding: gzip, compress, deflate, br
------------------------------------------------------------------------------------------------------------------------
HTTP/1.1 200 OK
age: 252936
cache-control: max-age=604800
content-type: text/html; charset=UTF-8
date: Mon, 06 Nov 2023 10:42:07 GMT
etag: "3147526947+gzip"
expires: Mon, 13 Nov 2023 10:42:07 GMT
last-modified: Thu, 17 Oct 2019 07:18:26 GMT
server: ECS (laa/7BA2)
vary: Accept-Encoding
x-cache: HIT
content-length: 648

<!doctype html>
<html>
<head>
    <title>Example Domain</title>

    <meta charset="utf-8" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style type="text/css">
    body {
        background-color: #f0f0f2;
        margin: 0;
        padding: 0;
        font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
        
    }
    div {
        width: 600px;
        margin: 5em auto;
        padding: 2em;
        background-color: #fdfdff;
        border-radius: 0.5em;
        box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
    }
    a:link, a:visited {
        color: #38488f;
        text-decoration: none;
    }
    @media (max-width: 700px) {
        div {
            margin: 0 auto;
            width: auto;
        }
    }
    </style>    
</head>

<body>
<div>
    <h1>Example Domain</h1>
    <p>This domain is for use in illustrative examples in documents. You may use this
    domain in literature without prior coordination or asking for permission.</p>
    <p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

------------------------------------------------------------------------------------------------------------------------

これでaxiosの通信ログを出力できるようになりましたが、使用するにあたっては注意が必要で、パスワードやアクセストークンなどの秘匿情報もログに出力されてしまうので、ローカル環境以外は無効にするか、マスク処理を実行する必要があります。

また、上記のサンプルコードは sandbox/ts-axios-logger · mrk21/sandbox にあります。