概要
クライアントサイド (Webブラウザ) から、Google Cloudのログ基盤であるCloud Loggingにログを書き出す方法について記載します。
昨今のWebシステムではクライアントサイドで複雑な処理を実装することが多く、クライアントサイドで発生した障害の把握や調査が難しくなっています。サーバ側にログを送ることによってこれらが可能になります。
Webブラウザ上で実行されるJavaScriptコードから直接Cloud Loggingに書き出すことはできないため、この記事ではFirebase Functionsを仲介として使用しています。
Firebase Functions
※ Firebaseプロジェクトの作成方法やFunctionsのデプロイ方法についての説明は割愛します。
ログをCloud Loggingに書き出す関数を作成します。
言語は任意ですが、以下の例ではnode (TypeScript) を使用しています。
Cloud LoggingのクライアントSDKをインストール
npm install @google-cloud/logging
関数定義
import * as admin from 'firebase-admin'
import * as functions from 'firebase-functions'
import { Logging } from '@google-cloud/logging'
admin.initializeApp()
export const writeLog = functions
.region('asia-northeast1')
.runWith({
timeoutSeconds: 10,
memory: '128MB',
maxInstances: 3,
})
.https.onCall(async ({ name, severity, payload }) => {
const logging = new Logging({
projectId: process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT,
})
const log = logging.log(name)
const entry = log.entry(
{
severity,
},
payload
)
await log.write(entry)
})
ここでは、writeLog
という関数を定義しています。
maxInstances
の指定によって、意図しない量の書き込みによってサーバコストが増大してしまうことを防止します。
パラメータとして、以下の3つが渡されてきます。
-
name
: 任意のログ名 (ログ閲覧時の絞り込みに使う) -
severity
: ログレベル (重大度) -
payload
: ログの内容 (任意のオブジェクト)
クライアントサイド
Firebase SDKのインストール
npm install firebase
Firebaseクライアント初期化処理
import { initializeApp } from 'firebase/app'
initializeApp({
// FirebaseのプロジェクトID
projectId: YOUR_PROJECT_ID,
// FirebaseのウェブAPIキー
apiKey: YOUR_API_KEY,
})
ロガーモジュール
※ 以下はFirebase SDK V9の例です。(V8以前とは若干異なります)
import { getFunctions, httpsCallable } from 'firebase/functions'
const functions = getFunctions()
functions.region = 'asia-northeast1'
const sendLog = httpsCallable(functions, 'writeLog')
const putLog = async (
position: string,
severity: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR',
message: unknown,
error?: unknown,
) => {
const payload: { [index: string]: unknown } = {
message,
severity,
url: location.href,
position,
userId: localStorage.getItem('userId'),
userAgent: navigator.userAgent,
}
if (error) {
if (error instanceof Error) {
payload.error = error.stack ?? `${error.name}: ${error.message}`
} else {
payload.error = String(error)
}
}
sendLog({
name: 'web_browser',
severity,
payload,
})
}
export default {
// debugレベルのログを出力する
debug: (
position: string,
message: unknown,
error?: unknown,
) => {
return putLog(position, 'DEBUG', message, error)
},
// infoレベルのログを出力する
info: (
position: string,
message:unknown,
error?: unknown,
) => {
return putLog(position, 'INFO', message, error)
},
// warnレベルのログを出力する
warn: (
position: string,
message: unknown,
error?: unknown,
) => {
return putLog(position, 'WARNING', message, error)
},
// errorレベルのログを出力する
error: (
position: string,
message: unknown,
error?: unknown,
) => {
return putLog(position, 'ERROR', message, error)
},
}
ログ出力内容は任意ですが、ここでは次の情報をpayload
に渡しています。
-
message
: ログメッセージ (文字列またはオブジェクト) -
url
: ブラウザが現在表示しているURL -
position
: ログ出力箇所を出力している情報 (ソースファイル名、メソッド名など) -
error
: エラー情報 (Errorオブジェクトなど) -
userId
: ユーザID -
userAgent
: ブラウザのユーザエージェント
使用例
import logger from './logger'
localStorage.setItem('userId', 'sample')
test()
function test() {
try {
// messageに文字列
logger.debug('app.ts|test', 'ああああ')
// messageにオブジェクト
logger.info('app.ts|test', {
foo: 'あいうえお',
bar: 12345,
})
throw Error('xxxx')
} catch (err) {
// errorにErrorオブジェクト
logger.error('app.ts|test', '○○○失敗', err)
}
}
ログの閲覧
GCP管理コンソールの「ログ エクスプローラ」で、書き出されたログを確認することができます。(反映されるまで、数十秒程度のタイムラグがあります)
Cloud Loggingでは、GCP全体のログが集められています。ログ名 (この記事の例ではweb_browser
としている) や、重大度 (severity) などによって絞り込むことができます。
注意
コスト
FunctionsやCloud Loggingには無料枠がありますが、一定量を超えると使用量が課せられます。特にBtoC系のシステムでは想定外の量が書き込まれる可能性があるため注意が必要です。
Cloud LoggingよりBigQueryの方が安価であるため、BigQueryを使う方がコストが抑えられます。(Functionsのコードの書き方はほぼ同じですが、BigQueryにテーブル定義などが必要になります)
また、状況によってはFunctionsの代わりにCloud RunでRESTful APIを実装した方が安くなる場合もあります。
プライバシー
不用意にユーザの個人情報やクライアント環境の情報を書き出さないようにしましょう。特に外国向けにサービスを提供する場合には、EU一般データ保護規則(GDPR) などに違反しないように注意してください。