マルチテナントSaaSにおけるログのことはじめ
あけましておめでとうございます。メディカルフォースCREチームでエンジニアをしているcocoです。自分は今年から厄年が始まるので、今年の目標は心身ともに健康に過ごすこととして、ジムに通いつつたくさん食事をすることを心がけています。まだ始めたばかりですが、日頃のデスクワークでなまりきった体にはすぐに効果が出ました(笑)
普段はCREチームでバグの解消・パフォーマンス改善をメインにチーム横断でのアーキテクチャ改善方針の設計やエンジニアメンバーが開発を行いやすいように足回りを整備することをやっています。今回の記事はその一環で行った構造化ログの導入についてお話しようと思います。
はじめに
弊社ではバグ発生時のログの調査で主にCloudWatch Logs Insightsを使用しています。
API Serverはlambda上で動いているため、バグが発生したリクエスト中のログを一つでも見つけることができれば、そのログにある@requestIdからリクエスト中に出力された一連のログを見ることができます。
それらのログを元にlocalでの再現などを行い、バグ修正を行うので、バグが発生したリクエスト中のログをどうにかして早く見つけることができればバグ解消までの時間を短縮できるわけです。
マルチテナントSaaSのログの特徴
マルチテナントSaaSでは全てのお客様が同じAPI Serverを利用するため、全てのお客様のログが混在して出力されます。しかし、API Serverにくるそれぞれの処理はお客様ごとに処理され、DBへの書き込み・取得も論理的に分割された状態で保存されます。つまり、論理的にテナントごとに別々の環境があるかのように動作しています。
バグ報告があったものを修正する場合バグが発生しているテナントは特定できているので、そのテナント以外の情報をフィルタリングすることで目的のログを見つけ出しやすくなります。
同じような思考で、アプリケーション使用者のIDや叩かれたAPIのpath, methodなども目的のログを見つけ出すための重要なヒントになります。
CloudWatch Logs InsightsにおけるJSONログ
CloudWatch Logs Insightsではjsonログを自動的にparseしてくれる機能があり、ログのフィルタリング・情報の収集をより楽に行うことができます。今回はこのメリットを利用して、テナントのIDであったり叩かれたAPIのpathやmethodなどの出力を行いました。
Loggerの実装
構造化ログの出力にあたって、Loggerの実装を行いました。目標はリッチすぎず、エンジニアメンバーがカジュアルに利用でき、とりあえず使っとけばメリットが享受できるものです。
リッチすぎないものにする理由は初期構想の段階でリッチなものにしすぎするとプロダクトが成長し、要件が増えて行った時に適合できなくなることを防ぐためです。
先に実装をあげておくと以下のようになりました。
import { LoginMiddleware } from '~/middlewares/common/LoginMiddleware';
const LogSeverity = {
DEFAULT: 'DEFAULT',
INFO: 'INFO',
WARN: 'WARN',
ERROR: 'ERROR',
} as const;
type Log = {
severity: (typeof LogSeverity)[keyof typeof LogSeverity];
message: string;
payload: unknown;
};
type LogParams = {
message: string;
payload: unknown;
};
export class Logger {
static log(params: LogParams): void {
Logger.write({
...params,
severity: LogSeverity.DEFAULT,
});
}
static info(params: LogParams): void {
Logger.write({
...params,
severity: LogSeverity.INFO,
});
}
static warn(params: LogParams): void {
Logger.write({
...params,
severity: LogSeverity.WARN,
});
}
static error(params: LogParams): void {
Logger.write({
...params,
severity: LogSeverity.ERROR,
});
}
private static write(log: Log): void {
const loginStatus = LoginMiddleware.status();
console.log({
...log,
auth: loginStatus,
});
}
}
次のように利用するイメージです
// ログを出す時
Logger.log('log message', {
// 関数の引数など欲しい情報を埋め込む
})
// エラー発生時
Logger.error('hogehoge error', {
// 関数の引数など欲しい情報を埋め込む
}, err)
要件
改めて要件を整理すると
- カジュアルに利用できる
- これを使っておけば先に上げた検索時に欲しい情報が埋め込まれる
カジュアルに利用できるという点ではログの出力はDBとのconnectionのように前もってすることも特にないので、普段使い慣れているconsole.logのIFにできるだけ近くするようにしました。
次に欲しい情報が埋め込まれると言う点で、認証情報が自動的に埋め込まれるようになっています。LoginMiddleware
を利用してauth情報を取得しているのがその部分です。
先にあげたjsonログに入れたい情報には含まれていませんが、Error Instanceを第三引数に渡すことで、messageとstacktraceの出力を行っています。これによって例外が投げられた時に発生箇所を容易に特定することができます。
振り返り
Good
- Loggerの実装がかなり軽いため、短時間で実装を終えることができた
- チームメンバーからバグ修正が楽になったとの声をいただいた
More
- 依然としてconsoleでログを出力している部分も多く、実装者がどちらを使えうべきか迷う状態なので統一するなり方針を決めた方が良かった
- 今時点で構造化ログの恩恵を受けているのはCloudWatch Logs Insightでの検索のみで、local開発では享受しきれていないので、localの構造化ログの便利な利用方法を周知した方が良かった(周知するMTG計画済み)
今後の展望
- 第三引数に持たせたError Instanceについてはstacktraceが出力されるが、
Logger.log
などを使用した場所の出力を行っていないので、行うことでさらに調査時間の短縮が見込める - 例外発生時にtry-catchで毎回ログを出力している部分が多く、ログの見通しが悪いのでError Classのcauseを利用するなどしてmiddlewareで一括で出力できるようにしたい
終わりに
今回はCREチームで行った構造化ログの導入について紹介させていただきました。このようなちょっとした改善でチームのパフォーマンスが上がるようにまだまだ弊社は改善できる部分がたくさんあります。
CREチームでは冒頭に挙げたタスクの他にもデータベースやサーバーの監視やコスト最適化なども行っています。また、スクラムチームでは2週間に1度の頻度で新機能をガンガンリリースしています。
個性豊かな面白いメンバーが集まってこれからの産業の成長プロセスを合理化すべく日々切磋琢磨しています。そんなメンバーたちと楽しく開発していきたい人を大募集中です!!!もし少しでも興味あればぜひカジュアル面談からお話しましょう!!!
Discussion