Electronでクラッシュ検知してログを送信する

Electronで色々と社内向けのアプリケーションを作成していますが、つい最近1例だけ、同じアプリで同じ環境にも関わらず、「起動しない」「イベントビューアにID 1000のクラッシュ記録が残ってる」という事例が。起動しないので、エラー出力もなく、開発環境で再現できない為、何が悪さをしてるのか?全く不明という状況。

他のマシンでは一切起きていない現象であり、当然クラッシュ記録も無い為、デバッグのしようがない・・・という事で、このクラッシュを検知してログを自動出力する仕組みを取り入れる事にしました。その記録です。

今回使用するモジュール

今回試しにPower Automateのインスタントクラウドフローを用意して受け取ってみようと思います。electron-logはローカルにログを出力するので、これをPower Automateに送信して、OneDriveにファイルを保存し、Teamsに通知してみたいと思います。

モジュールのインストール

インストール

プロジェクトフォルダに移動してから、ターミナル(コマンドプロンプト)より以下のコマンドでインストールします。

npm install --save electron-log

ロードする

2つのモジュールをロードする記述をindex.jsの冒頭に追加します。

//クラッシュレポートをロードする
var logger = require('electron-log');

次項でそれぞれのモジュールの使い方を記述しています。

意図的にクラッシュさせる

開発テストの間でだけ使う「意図的にクラッシュさせる」コードは以下の通り。index.jsのポイント(例えば、mainWindowを呼び出した直後とか、app初期化時など)に配置して、クラッシュさせます。

以下は初期化が完了したあとにクラッシュさせるようにしています。

//初期化終了後に例外を出力して引っ掛ける
app.whenReady().then(()=>{
     //3秒後にクラッシュさせる
     setTimeout(()=>{
        throw new Error('なにかとんでもないエラーが。。。')
     }, 3000)
})

各モジュールの使い方

electron-logについて

エラーログの保存先

各OSによって、ログの出力先が異なります。今回はWindowsでテストしているので、Roaming以下のディレクトリにあるファイルを拾います。

  • Windows :C:\Users\AppData\Roaming\アプリの名前\logs\プロセスタイプ.log
  • macOS : ~/Library/Logs/アプリの名前/プロセスタイプ.log
  • Linux : ~/.config/アプリの名前/logs/プロセスタイプ.log

このエラーログのファイル名を予め指定しておいて、出力する事で後で調べるときにより便利になるので、以下のコードをクラッシュ検知の前に用意しておきます。以下のコードで、例:20220512.logというファイル名でファイルが出力されます。

log.transports.file.resolvePathで任意のフォルダに出力できます。またファイル名だけを変えたい場合には、log.transports.file.fileNameで変更可能。

//マイドキュメントパスを指定
var dir_home = process.env[process.platform == "win32" ? "USERPROFILE" : "HOME"];
var docupath = require("path").join(dir_home, "Documents");

//クラッシュログのファイル名を指定する
(() => {
  //日付を取得する
  const today = new Date();

  //日付を整形
  var year = ('0000' + today.getFullYear()).slice(-4);
  var month = ('00' + (today.getMonth()+1)).slice(-2);
  var day = ('00' + today.getDate()).slice(-2);

  //ファイル名を生成
  var reportname = year + month + day + '.log';

  //出力先をデスクトップに変更
  log.transports.file.resolvePath = () => docupath + "//" + reportname;
})();

クラッシュ検知をさせてログを出力する

例外を投げてエラーをわざと引き起こして、エラートラップさせます。エラーを取得したら、log.error(err)にて、electron-logにログ出力をさせます。

//エラートラップ
process.on('uncaughtException', function(err) {
  console.log("error trap");
  log.error(err.stack);;
  app.quit();  //プログラムを終了する
});

出力された内容は以下のように出力されて、同じファイル名のファイルが有った場合には、追記の形で書き込みがされます。

[2022-05-20 12:32:06.505] [error] Error: とんでもないエラーが。。。。
    at Timeout._onTimeout (C:\Users\googl\Documents\tscheck\index.js:317:15)
    at listOnTimeout (internal/timers.js:554:17)
    at processTimers (internal/timers.js:497:7)

エラーログを送信

エラーログをメールで送ってしまう

electron-logはログを出力して仕事は終了です。しかし、開発者としてはそのログを速攻で送ってもらったほうがありがたいので、メールで送って欲しいのですがその為の仕組みとしては、WindowsならばElectronからVBS叩いて送ってしまったほうがどんな環境でも動作するので楽です。以下はVBSに引数でログファイルのパスを渡して、メールに添付して送りつけるという手法です。

VBSと連携するElectronアプリを作る

Outlookを使ってVBSにて送信する
'引数を取得する
Dim args : args = WScript.Arguments(0)

'変数を宣言し、引数を取得する
dim status : status = 1
Dim filepath : filepath = args

'エラー発生時に処理を継続
On Error Resume Next

'送信先アドレス等
Dim mail : mail = "ここに送信先アドレスを入れる"

'Outlookの準備
Dim objApp
Dim objMail
Set objApp = CreateObject("Outlook.Application")
Set objMail = objApp.CreateItem(0)

'メールの送信設定
objMail.to = mail
objMail.Subject = "例外エラー通知"
objMail.BodyFormat = 1
objMail.Body = "TSCheckに於いて例外を検知したので、ログを送ります。"

'添付ファイル
objMail.Attachments.Add filepath

'送信
objMail.Send

'オブジェクトの開放
Set objMail = Nothing
Set objApp = Nothing

'無事完了したのでステータスを3にする
status = 3

'エラー時ジャンプ先
On Error Goto 0

'ステータスを返す
WScript.Echo status
WScript.Quit status

Electronからは生成したログのフルパスを引数としてVBS側に渡すだけ。返り値が3ならば成功、1ならばエラーとして判定して処理をすれば良い。

Electron側でVBSを呼び出すコード
//vbs実行用(同期的にexe実行を行う)
var spawnSync = require('child_process').spawnSync;

//外部コマンドを実行する
async function vbsCommand(fullpath, callback) {
  //実行するVBSファイルを指定
 let vbsfile = __dirname + '/vbs/エラー送信.vbs'

  //コマンドを組み立てて実行
  var child = spawnSync('cscript.exe', [vbsfile, fullpath]);

  //返り値を取得する(status)
  var ret = child.status;

  //retを返す
  callback(ret)
}
  • VBS側に引数でログファイルのフルパスを送りつけています
  • cscript.exeで実行するVBSファイルとフルパスを基に実行しています。実行が完了するまで待機します。

Power Automateに送りつける

生成されたログをOutlookで送るという手段は今風ではないのと、VBSでメールにて送信という手段故に、社内のメールフィルタに阻害される可能性もあるため、これをPower Automateに用意したフローで受け取り、Teams通知⇒OneDrive Businessに保存という手段を構築したいと思います。尚、ファイル名が同じ場合には上書きされるので、ファイル名の付け方には注意が必要です(特に複数名いる場合には、日付だけだと重複する可能性があるので日時秒まで入れてみるとか、ユーザ名を入れてみるとか)

Power Automate側

まずは、エラーログを受け取るフローを作成します。エラー内容のテキストはJSONで送られてくるので、それをパースして、ファイルにして保存するまでのフローです。

  1. Power Automateにてインスタントクラウドフローを作成する
  2. トリガーはHTTP要求の受信時を選択し、作成をクリック
  3. 詳細オプションを開き、MethodはPOSTを選択
  4. JSONスキーマは以下のようなものを入れておきます。
    {
        "type": "object",
        "properties": {
            "error": {
                "type": "string"
            },
            "name": {
                "type": "string"
            }
        }
    }

    errorがエラー内容、nameがファイル名としてElectronから送信します。

  5. 続けて、データ操作で検索して、作成コネクタを追加します
  6. 作成の入力は、2.のフローのerrorを選びます
  7. 次に、OneDrive Businessで検索して、ファイルの作成コネクタを選びます。
  8. フォルダのパスは保存先のフォルダを選んでおきます
  9. ファイル名は2.のフローのnameを選びます
  10. ファイルコンテンツは、5.のフローの出力を選びます
  11. 続けて、Teamsで検索し、「チャットまたはチャンネルでメッセージを投稿する」コネクタを選択します。
  12. 投稿者、投稿先、チーム、チャンネルを選択し、件名を入力します。
  13. メッセージ本文は自分は以下のようなものを利用しています。直接リンクからファイルを開ける用に、予め自分のドメインでのOneDriveのそのファイルへのリンクを基に、7.のパスを利用して構築しています。
    ○○からエラーが送信されてきました。<br><br>
    <a href="https://ドメイン.sharepoint.com/:u:/r/personal/ユーザ名/Documents/@{outputs('ファイルの作成')?['body/Path']}?csf=1&web=1&e=bCcxKI">ファイルを開く</a>

    ドメインは自社ドメイン、ユーザ名は自分のメアド、そこにパスのコネクタをつなげて、最後にcsf=以下をつなげたものをURLとして、ハイパーリンクとしています。csf=以下が無いとダウンロードになります。

  14. 最後に、トリガーとなったHTTP要求の受信時コネクタに生成されたURLをコピーしておきます。これをElectron側で利用します。

図:プレミアムコネクタを利用します

図:Teamsに通知が飛んできた

図:OneDriveに自動生成されたファイル

図:後半のフロー全体図

Electron側

Electron側は、前項の「クラッシュ検知をさせてログを出力する」の項目にて、利用したトラップの関数内にnode-fetchを利用してPOSTでerror.logを添付して送るようにしています。

//モジュール
const fetch = require('node-fetch');
var ProxyAgent = require('proxy-agent');

//プロキシ設定
var agent = new ProxyAgent(ココにプロキシのURLを入れる);

//エラートラップ
process.on('uncaughtException', function(err) {
  //エラーログ出力
  console.log("error trap");
  log.error('electron:event:uncaughtException');
  log.error(err);
  log.error(err.stack);

  const buffer = fs.readFileSync(reportname, 'utf-8')
  const fileName = "ユーザ名" + "_" + "20220512.log";

  //Power Automate側endpoint
  var papoint = "ここにPower Automate側のHTTPのURLを入れる";

  //HTTPリクエスト
  var status = "";

  //リクエストボディ
  var body = {
    error:buffer,
    name:fileName
  }

  //リクエストヘッダ
  var headers = {
    'Content-Type': 'application/json'
  }

  //送信オプション
  var options = {
    method: 'post',
    agent: agent,
    headers: headers,
    body:JSON.stringify(body)
  }

  fetch(papoint, options)
  .then((res) => { 
    //ステータスコードを取得
    status = res.status; 

    //body部分を取得
    return res;
  }).then((jsonData) => {
    if(status == 202){
      //ログ送信したダイアログを表示
      //最新版が出ている場合の処理
      var options = {
        type: 'info',
        buttons: ['OK'],
        title: 'エラーログ送信',
        message: 'エラー内容を開発者に送信しました。',
        detail: 'エラーをサーバに送信しました。'
      };

      //ダイアログの回答を元に処理を分岐
      var resdia = dialog.showMessageBoxSync(mainWindow, options);
    }else{
      //ログ送信失敗

    }
  }).catch((err) => {
    //エラーメッセージ
    console.log(err + "ですよ")
    return "NG";
  });

  //app.quit();
});
  • 生成されたファイルの内容をfs.readFileSyncにて読み取ります。
  • filenameは予め日付とユーザ名を組み合わせたものを用意しておきます。
  • node-fetchにてPOSTにて送信します。この時papointに前項で取得したHTTP要求の受信時のURLを入れておきます。
  • agentはProxy経由する場合に必要なものなので、不要であれば削除します。
  • リクエストが成功すると、Power Automate側からは、202コードが返ってきます。
  • 送信したら、その旨のダイアログを表示するようにしています。

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)