0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

自動化スクリプトが止まっても誰も気づかない問題:エラー通知と死活監視の作り方【GAS・cron対応】

0
Posted at

はじめに

  • 毎朝届くはずの集計メールが、気づいたら2週間止まっていた
  • cronで回していたスクリプトが、サーバーを移行した日から一度も動いていなかった
  • エラー通知は入れていたのに、そもそも起動していなかったので何も届かなかった

業務の自動化は、作ったときより止まったときに気づけるかのほうが大事です。止まったことに誰も気づかなければ、手作業に戻すことすらできません。

この記事では、GASとcronのジョブをまとめて見張る仕組みを、Googleスプレッドシート+GASだけで作ります。有料の監視サービスは使いません。

完成イメージ

エラー通知だけでは足りない理由

ジョブの止まり方は1つではありません。

止まり方 よくある原因 エラー通知で気づける? 死活監視で気づける?
処理中にエラーが出る API仕様の変更、認証切れ、データ不正 ○ ○
そもそも起動しない トリガーが消えた、cronの設定ミス、サーバー移行 × ○
途中で固まって終わらない 外部APIの応答待ち、無限ループ △ ○(タイムアウト設定で検知)
正常終了したのに中身が空 取り込み元が0件、条件の指定ミス × × → ジョブ側で件数をチェックする

「失敗したら知らせる」だけでは、「動いていない」ことには気づけません。
そこで、次の3つを組み合わせます。

  1. エラー通知:失敗したらSlackかメールに通知する
  2. 死活監視:「最後に成功した時刻」を記録しておき、一定時間更新がなければ通知する
  3. ログ:いつ・何が・何秒で終わったかを残し、後から追えるようにする

全体の構成

[GASのジョブ] ── runJob() で包む ─────────────┐
                                             ├─> [監視スプレッドシート]
[cronのジョブ] ── monitor_run.py ──(HTTP)────┘        ↑
                                              checkHeartbeats()(毎時)
                                                       ↓
                                               Slack / メール通知

ポイントは、監視をする側(GAS)と監視される側(サーバー)を別の場所で動かすことです。サーバーが丸ごと落ちても、Google側の監視は動き続けるので「止まった」ことに気づけます。

届く通知の例

🚨 [失敗] order-import
exit=1
PHP Fatal error: Uncaught Exception: API returned 401 ...
⏰ [停止の疑い] 日次売上集計
最終成功: 2026/10/05 07:00(許容間隔 1500分)
✅ [復旧] order-import

通知は状態が変わったときだけ送ります。10分おきに失敗するジョブがあっても、通知は「失敗」と「復旧」の2通で済みます。

監視シートのイメージ

ジョブ名 許容間隔(分) 最終成功 最終実行 状態 連続失敗 最終メッセージ
日次売上集計 1500 2026/10/07 07:00 2026/10/07 07:00 OK 0
order-import 90 2026/10/07 09:00 2026/10/07 10:00 ERROR 1 exit=1 ...

準備 / 前提

  • Googleアカウント(無料アカウントで動きます)
  • 通知先(どちらか)
    • Slackの Incoming Webhook URL
    • メールアドレス
  • cronのジョブも監視する場合
    • Python 3.6以上が使えるサーバー(標準ライブラリだけで動くので pip install は不要です)

Slackの Incoming Webhook URL は、https://api.slack.com/apps で「Create New App」→「Incoming Webhooks」を有効化→「Add New Webhook to Workspace」で通知先のチャンネルを選ぶと発行されます。

Step 1:監視用のスプレッドシートとGASを用意する

  1. 新しいGoogleスプレッドシートを作成する(名前は「ジョブ監視」など)
  2. URLの https://docs.google.com/spreadsheets/d/【ここ】/edit の部分がIDになるので、控えておく
  3. 「拡張機能」→「Apps Script」を開き、最初からある コード.gs の中身を以下にすべて置き換える
  4. ★ の付いた設定値を書き換える
/**
 * 自動化ジョブの監視(エラー通知 + 死活監視 + ログ)
 */
const CONFIG = {
  SPREADSHEET_ID: 'ここにスプレッドシートIDを貼る', // ★
  SLACK_WEBHOOK_URL: '',                         // ★Slackに通知する場合はWebhook URL。空ならメールで通知
  NOTIFY_EMAIL: 'you@example.com',               // ★通知先(Slackへの送信に失敗したときの予備にも使う)
  TOKEN: 'ここに推測されにくい長い文字列',          // ★cron側から報告するときの合言葉
  DEFAULT_ALLOWED_MIN: 1500,                     // 新しいジョブの許容間隔(分)。1500分 = 25時間
  LOG_KEEP_ROWS: 5000,                           // ログシートに残す行数
  MONITOR_SHEET: '監視',
  LOG_SHEET: 'ログ',
};

// ===== 初期設定(最初に1回だけ手動で実行する) =====
function setup() {
  const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
  ensureSheet_(ss, CONFIG.MONITOR_SHEET,
    ['ジョブ名', '許容間隔(分)', '最終成功', '最終実行', '状態', '連続失敗', '最終メッセージ']);
  ensureSheet_(ss, CONFIG.LOG_SHEET, ['日時', 'ジョブ名', '結果', '所要秒', 'メッセージ']);

  // 何度実行してもトリガーが重複しないよう、既存のものを消してから作り直す
  ScriptApp.getProjectTriggers()
    .filter(t => ['checkHeartbeats', 'weeklySummary'].includes(t.getHandlerFunction()))
    .forEach(t => ScriptApp.deleteTrigger(t));
  ScriptApp.newTrigger('checkHeartbeats').timeBased().everyHours(1).create();
  ScriptApp.newTrigger('weeklySummary').timeBased()
    .onWeekDay(ScriptApp.WeekDay.MONDAY).atHour(9).create();
}

// ===== GAS内のジョブを包む =====
function runJob(jobName, fn) {
  const start = Date.now();
  try {
    fn();
    report_(jobName, 'OK', elapsed_(start), '');
  } catch (e) {
    report_(jobName, 'ERROR', elapsed_(start), (e && e.stack) ? e.stack : String(e));
    throw e; // GASの「実行数」画面にも失敗として残す
  }
}

// ===== cronなど外部からの報告を受け付ける =====
function doPost(e) {
  let body;
  try {
    body = JSON.parse(e.postData.contents);
  } catch (err) {
    return json_({ ok: false, error: 'invalid json' });
  }
  if (body.token !== CONFIG.TOKEN) {
    return json_({ ok: false, error: 'unauthorized' });
  }
  const job = String(body.job || '').slice(0, 50);
  if (!job || /^[=+\-@]/.test(job) || !['OK', 'ERROR'].includes(body.status)) {
    return json_({ ok: false, error: 'bad request' });
  }
  report_(job, body.status, Number(body.seconds) || 0, body.message || '');
  return json_({ ok: true });
}

// ===== 死活監視(毎時トリガーで実行) =====
function checkHeartbeats() {
  const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
  const sheet = ss.getSheetByName(CONFIG.MONITOR_SHEET);
  const alerts = [];
  const lock = LockService.getScriptLock();
  lock.waitLock(30000);
  try {
    const last = sheet.getLastRow();
    if (last >= 2) {
      const range = sheet.getRange(2, 1, last - 1, 7);
      const rows = range.getValues();
      const now = Date.now();
      rows.forEach(r => {
        const [name, allowedMin, lastOk, , status] = r;
        if (!name || !allowedMin || !(lastOk instanceof Date)) return; // 一度も成功していないジョブは、初回の成功から監視する
        if (['STALE', 'ERROR', 'PAUSED'].includes(status)) return;      // 通知済み、または一時停止中
        if (now - lastOk.getTime() > allowedMin * 60 * 1000) {
          r[4] = 'STALE';
          alerts.push(`⏰ [停止の疑い] ${name}\n最終成功: ${fmt_(lastOk)}(許容間隔 ${allowedMin}分)`);
        }
      });
      if (alerts.length) range.setValues(rows);
    }
    trimLogs_(ss);
  } finally {
    lock.releaseLock();
  }
  if (alerts.length) notify_(alerts.join('\n\n'));
}

// ===== 週次レポート(監視そのものが動いている証拠になる) =====
function weeklySummary() {
  const sheet = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID).getSheetByName(CONFIG.MONITOR_SHEET);
  const last = sheet.getLastRow();
  const rows = last >= 2 ? sheet.getRange(2, 1, last - 1, 7).getValues() : [];
  const lines = rows
    .filter(r => r[0])
    .map(r => `[${r[4]}] ${r[0]}(最終成功: ${r[2] instanceof Date ? fmt_(r[2]) : '-'})`);
  notify_(`📋 週次監視レポート(監視は稼働中です)\n${lines.join('\n') || '登録されたジョブはありません'}`);
}

// ===== 動作確認用 =====
function testOk() {
  runJob('テスト', () => Logger.log('正常に終わる処理'));
}

function testError() {
  runJob('テスト', () => { throw new Error('わざと失敗させています'); });
}

// ===== 内部処理 =====
function report_(jobName, status, seconds, message) {
  const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
  const msg = safe_(message);
  let alert = '';
  const lock = LockService.getScriptLock();
  lock.waitLock(30000);
  try {
    const now = new Date();
    ss.getSheetByName(CONFIG.LOG_SHEET).appendRow([now, jobName, status, seconds, msg]);

    const sheet = ss.getSheetByName(CONFIG.MONITOR_SHEET);
    const range = sheet.getRange(findOrCreateRow_(sheet, jobName), 1, 1, 7);
    const v = range.getValues()[0];
    const prev = v[4];
    v[3] = now;
    v[4] = status;
    if (status === 'OK') {
      v[2] = now;
      v[5] = 0;
    } else {
      v[5] = (Number(v[5]) || 0) + 1;
    }
    v[6] = msg;
    range.setValues([v]);

    // 状態が変わったときだけ通知する(同じエラーを何十通も送らない)
    if (status === 'ERROR' && prev !== 'ERROR') {
      alert = `🚨 [失敗] ${jobName}\n${msg}`;
    } else if (status === 'OK' && (prev === 'ERROR' || prev === 'STALE')) {
      alert = `✅ [復旧] ${jobName}`;
    }
  } finally {
    lock.releaseLock();
  }
  if (alert) notify_(alert);
}

function findOrCreateRow_(sheet, jobName) {
  const last = sheet.getLastRow();
  if (last >= 2) {
    const names = sheet.getRange(2, 1, last - 1, 1).getValues();
    for (let i = 0; i < names.length; i++) {
      if (names[i][0] === jobName) return i + 2;
    }
  }
  sheet.appendRow([jobName, CONFIG.DEFAULT_ALLOWED_MIN, '', '', 'NEW', 0, '']);
  return sheet.getLastRow();
}

function notify_(text) {
  try {
    if (CONFIG.SLACK_WEBHOOK_URL) {
      UrlFetchApp.fetch(CONFIG.SLACK_WEBHOOK_URL, {
        method: 'post',
        contentType: 'application/json',
        payload: JSON.stringify({ text }),
      });
    } else {
      MailApp.sendEmail(CONFIG.NOTIFY_EMAIL, '[監視] ' + text.split('\n')[0], text);
    }
  } catch (e) {
    // Slackに送れなかったときはメールで知らせる
    MailApp.sendEmail(CONFIG.NOTIFY_EMAIL, '[監視] 通知の送信に失敗しました', text + '\n\n' + e);
  }
}

function ensureSheet_(ss, name, headers) {
  const sheet = ss.getSheetByName(name) || ss.insertSheet(name);
  if (sheet.getLastRow() === 0) {
    sheet.appendRow(headers);
    sheet.setFrozenRows(1);
  }
}

function trimLogs_(ss) {
  const sheet = ss.getSheetByName(CONFIG.LOG_SHEET);
  const excess = sheet.getLastRow() - 1 - CONFIG.LOG_KEEP_ROWS;
  if (excess > 0) sheet.deleteRows(2, excess);
}

function safe_(s) {
  const str = String(s || '').slice(0, 1000);
  return /^[=+\-@]/.test(str) ? "'" + str : str; // 先頭が「=」などだと数式として扱われるのを防ぐ
}

function elapsed_(start) {
  return Math.round((Date.now() - start) / 100) / 10;
}

function fmt_(date) {
  return Utilities.formatDate(date, Session.getScriptTimeZone(), 'yyyy/MM/dd HH:mm');
}

function json_(obj) {
  return ContentService.createTextOutput(JSON.stringify(obj))
    .setMimeType(ContentService.MimeType.JSON);
}

タイムゾーンを日本時間にする

通知に出る時刻と週次レポートの送信時刻は、プロジェクトのタイムゾーンに従います。

  1. 左メニューの「⚙ プロジェクトの設定」→「appsscript.json マニフェスト ファイルをエディタで表示する」にチェックを入れる
  2. エディタに出てきた appsscript.json の timeZone を "Asia/Tokyo" にする

setup を実行する

関数の選択欄で setup を選んで「実行」します。初回は権限の承認画面が出るので許可してください。

スプレッドシートに「監視」と「ログ」のシートができ、トリガーが2つ(毎時の死活監視と、月曜9時の週次レポート)登録されれば完了です。

Step 2:GASのジョブで動作を確認する

testOk → testError → testError → testOk の順に実行してみてください。

実行する関数 監視シートの状態 通知
testOk NEW → OK なし
testError OK → ERROR 🚨 失敗
testError(2回目) ERROR のまま(連続失敗 2) なし(同じ状態なので送らない)
testOk ERROR → OK ✅ 復旧

testError は実行画面でも赤いエラーになりますが、これは runJob がエラーを投げ直しているためで、意図どおりの動きです。

Step 3:許容間隔を設定する

監視シートのB列「許容間隔(分)」には、最後に成功してから何分経ったら「止まっている」とみなすかを入れます。新しいジョブは初期値の1500分(25時間)で登録されます。

ジョブの実行間隔 許容間隔(分)の目安 考え方
10分ごと 30 2回続けて抜けたら異常
毎時 90 1回抜けたら異常
毎日 1500 25時間。実行時刻の多少のズレを吸収する
平日のみ毎日 4380 金曜の朝から月曜の朝までの73時間
毎月1日 46080 32日

死活監視は1時間ごとに動くので、検知が最大1時間ほど遅れる点は織り込んでおいてください。

意図的に止めているジョブは、E列の状態を PAUSED に書き換えると監視の対象から外れます。ジョブから再び報告が届くと、自動で監視が再開されます。

トリガーの種類や設定方法の詳細は、GASのトリガー完全ガイドにまとめています。

Step 4:Webアプリとしてデプロイする(cronのジョブ用)

cronで動かしているジョブからも報告を受けられるように、doPost を公開します。

  1. 右上の「デプロイ」→「新しいデプロイ」
  2. 種類の選択(⚙)で「ウェブアプリ」を選ぶ
  3. 次のように設定してデプロイする
    • 次のユーザーとして実行:自分
    • アクセスできるユーザー:全員
  4. 表示された「ウェブアプリのURL」(https://script.google.com/macros/s/.../exec)を控える

「全員」に公開しても、TOKEN が一致しない報告は受け付けないようにしてあります。TOKEN は推測されにくい長い文字列にしてください。

curlで動作を確認する

サーバーのターミナルから、次のコマンドで報告を送れるか試します。

curl -L \
  -H "Content-Type: application/json" \
  -d '{"token":"ここにTOKENと同じ値","job":"curl-test","status":"OK","seconds":0}' \
  "https://script.google.com/macros/s/xxxxxxxx/exec"

{"ok":true} が返り、監視シートに curl-test の行が増えれば成功です。

Step 5:cronのジョブを監視ラッパー経由で動かす

どんなコマンドでも包めるラッパーをPythonで書きます。PHPでもシェルスクリプトでも、終了コードで成功か失敗かを判定します。

monitor_run.py として保存してください。

#!/usr/bin/env python3
"""
任意のコマンドを実行して、結果をGASの監視APIに報告するラッパー

使い方:
  python3 monitor_run.py ジョブ名 -- 実行するコマンド [引数...]
例:
  python3 monitor_run.py order-import -- /usr/bin/php /home/USER/app/import_orders.php
"""
import json
import os
import subprocess
import sys
import time
import urllib.request
from datetime import datetime

CONFIG = {
    "ENDPOINT": "https://script.google.com/macros/s/xxxxxxxx/exec",  # ★GASのWebアプリのURL
    "TOKEN": "ここにTOKENと同じ値",  # ★GAS側のCONFIG.TOKENと同じ値
    "TIMEOUT_SEC": 1800,  # ★ジョブの最大実行時間(秒)。超えたら失敗として扱う
    "LOG_FILE": "/home/USER/logs/monitor_run.log",  # ★ローカルログの保存先
}


def log(line):
    stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    try:
        os.makedirs(os.path.dirname(CONFIG["LOG_FILE"]), exist_ok=True)
        with open(CONFIG["LOG_FILE"], "a", encoding="utf-8") as f:
            f.write(f"[{stamp}] {line}\n")
    except OSError as e:
        print(f"[{stamp}] ログを書き込めません: {e} / {line}", file=sys.stderr)


def report(job, status, seconds, message):
    payload = json.dumps({
        "token": CONFIG["TOKEN"],
        "job": job,
        "status": status,
        "seconds": seconds,
        "message": message,
    }, ensure_ascii=False).encode("utf-8")
    req = urllib.request.Request(
        CONFIG["ENDPOINT"],
        data=payload,
        headers={"Content-Type": "application/json"},
    )
    for attempt in range(1, 4):
        try:
            with urllib.request.urlopen(req, timeout=30) as res:
                body = json.loads(res.read().decode("utf-8"))
            if body.get("ok"):
                return True
            log(f"報告が拒否されました: {body}")
            return False
        except Exception as e:
            log(f"報告に失敗しました({attempt}/3): {e}")
            time.sleep(5 * attempt)
    return False


def main():
    if len(sys.argv) < 4 or sys.argv[2] != "--":
        print("usage: monitor_run.py JOB_NAME -- COMMAND [ARGS...]", file=sys.stderr)
        sys.exit(2)

    job = sys.argv[1]
    cmd = sys.argv[3:]
    start = time.time()

    try:
        proc = subprocess.run(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=CONFIG["TIMEOUT_SEC"],
        )
        code = proc.returncode
        output = (proc.stderr or proc.stdout).decode("utf-8", errors="replace")
    except subprocess.TimeoutExpired:
        code = -1
        output = f"タイムアウトしました({CONFIG['TIMEOUT_SEC']}秒)"
    except FileNotFoundError as e:
        code = 127
        output = f"コマンドが見つかりません: {e}"

    seconds = round(time.time() - start, 1)
    status = "OK" if code == 0 else "ERROR"
    message = "" if code == 0 else f"exit={code}\n{output[-800:]}"

    log(f"{job} {status} {seconds}s exit={code}")
    if code != 0:
        log(output[-2000:])

    report(job, status, seconds, message)
    sys.exit(code if code > 0 else (0 if code == 0 else 1))


if __name__ == "__main__":
    main()

crontab に登録する

既存のcron行の先頭に、ラッパーを挟むだけです。

# 変更前
0 * * * * /usr/bin/php /home/USER/app/import_orders.php

# 変更後(flockで多重起動を防ぎ、cron自体の出力もログに残す)
0 * * * * /usr/bin/flock -n /home/USER/tmp/order-import.lock /usr/bin/python3 /home/USER/bin/monitor_run.py order-import -- /usr/bin/php /home/USER/app/import_orders.php >> /home/USER/logs/cron.log 2>&1
  • python3 や php のパスはサーバーごとに違うので、which python3、which php で確認してください
  • /home/USER/tmp のようなロック用のディレクトリは、あらかじめ作っておいてください

ジョブを報告に乗せたら、初回の実行後に監視シートの許容間隔をそのジョブに合わせて直します(この例は毎時なので 90)。

GAS・cron・GitHub Actions のどこでジョブを動かすか迷っている場合は、定期実行どこで動かす?GAS・cron・GitHub Actionsの使い分けも参考にしてください。

実用例

1. 既存のGASを包み、「0件」も失敗として扱う

監視用のGASプロジェクトの中で、既存の処理を runJob で包みます。冒頭の表にあった「正常終了したのに中身が空」を拾うため、件数のチェックを入れておくのがおすすめです。

function dailySalesReport() {
  runJob('日次売上集計', () => {
    const sheet = SpreadsheetApp.openById('売上シートのID').getSheetByName('売上'); // ★
    const rowCount = sheet.getLastRow() - 1;
    if (rowCount <= 0) {
      throw new Error('売上データが0件です。取り込み元を確認してください');
    }
    const values = sheet.getRange(2, 3, rowCount, 1).getValues(); // ★C列が金額の場合
    const total = values.reduce((sum, [v]) => sum + (Number(v) || 0), 0);
    MailApp.sendEmail(CONFIG.NOTIFY_EMAIL, '日次売上集計', `合計: ${total.toLocaleString()}円(${rowCount}件)`);
  });
}

トリガーには dailySalesReport を登録します。

2. 別のGASプロジェクトから報告する

監視対象のGASが別のプロジェクトにある場合は、そのプロジェクトに次のコードを追加し、Webアプリに報告します。

const MONITOR = {
  ENDPOINT: 'https://script.google.com/macros/s/xxxxxxxx/exec', // ★監視用GASのWebアプリのURL
  TOKEN: 'ここにTOKENと同じ値',                                   // ★
};

function withMonitor(jobName, fn) {
  const start = Date.now();
  try {
    fn();
    sendReport_(jobName, 'OK', start, '');
  } catch (e) {
    sendReport_(jobName, 'ERROR', start, (e && e.stack) ? e.stack : String(e));
    throw e;
  }
}

function sendReport_(jobName, status, start, message) {
  UrlFetchApp.fetch(MONITOR.ENDPOINT, {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify({
      token: MONITOR.TOKEN,
      job: jobName,
      status: status,
      seconds: Math.round((Date.now() - start) / 100) / 10,
      message: message,
    }),
    muteHttpExceptions: true,
  });
}

// 使い方
function myDailyJob() {
  withMonitor('請求データ取り込み', () => {
    Logger.log('ここで既存の処理を呼び出す');
  });
}

3. 監視そのものが止まったときに気づく

監視の仕組みも、いつかは止まります。そこで二重に備えておきます。

  • 週次レポート:毎週月曜9時に「監視は稼働中です」というレポートが届きます。届かない週があれば、監視が止まっていると判断できます
  • GASのトリガー失敗通知:Apps Scriptの「トリガー」画面で checkHeartbeats を開き、「エラー通知設定」を「すぐに通知を受け取る」にしておくと、監視自体がエラーになったときにGoogleからメールが届きます

つまずきやすいポイント

コードを直したのに、Webアプリの動きが変わらない

Webアプリはデプロイした時点のコードで動きます。コードを修正したら「デプロイ」→「デプロイを管理」→ 鉛筆アイコン →「バージョン:新バージョン」で更新してください。
「新しいデプロイ」を作るとURLが変わってしまうため、cron側の設定も書き換えが必要になります。

curlでエラーページのHTMLが返ってくる

GASのWebアプリは、POSTを受けると別のURLへリダイレクトして結果を返します。

  • -L を付けないと、リダイレクトをたどらずに終わってしまう
  • -X POST を付けると、リダイレクト先にもPOSTしてしまい、エラーになる

Step 4のように、-L と -d だけを付けてください。Pythonの urllib とGASの UrlFetchApp は、リダイレクトを自動で正しく処理します。

{"ok":false,"error":"unauthorized"} が返る

GAS側の CONFIG.TOKEN と、Python側の CONFIG["TOKEN"] が一致していません。前後に空白が入っていないかも確認してください。

毎週月曜に「停止の疑い」が届く

平日だけ動くジョブの許容間隔が短すぎます。金曜から月曜までの空きを考慮して、Step 3の表を目安に広げてください。

通知が届かない

  • Slack:Webhook URLが正しいか確認してください。Slackへの送信に失敗した場合は NOTIFY_EMAIL 宛てにメールが届きます
  • メール:無料のGoogleアカウントでは、GASから送れるメールは1日100通前後が上限です。通知を「状態が変わったときだけ」にしているのは、この上限に引っかからないためでもあります
  • どちらも届かない:Apps Scriptの「実行数」画面で checkHeartbeats がエラーになっていないか確認してください

Lock timeout のエラーが出る

多くのジョブが同じ時刻(0 * * * * など)に一斉に報告すると、ロック待ちが発生します。cronの実行時刻を数分ずつずらしてください。

cronのジョブ名が文字化けする、cronが動かない

  • crontabでは、ジョブ名は order-import のように半角英数字とハイフンにしておくと安全です
  • crontabの中で % は特別な意味を持ちます。date +%Y%m%d などを書く場合は \% とエスケープしてください

トークンをコードに直書きしたくない

CONFIG.TOKEN や SLACK_WEBHOOK_URL は、スクリプトプロパティ(「プロジェクトの設定」→「スクリプト プロパティ」)に保存し、PropertiesService.getScriptProperties().getProperty('TOKEN') で読み込む形にもできます。Gitで管理する場合は、こちらにしてください。

まとめ

  • エラー通知だけでは**「起動していない」ことに気づけない**。死活監視(最後に成功した時刻のチェック)とセットで考える
  • 監視する側(GAS)と監視される側(サーバー)を別の場所で動かすと、サーバーが丸ごと落ちても気づける
  • 通知は状態が変わったときだけ送る。通知が多すぎると、そのうち誰も見なくなる
  • 「0件で正常終了」はジョブ側で件数をチェックして、失敗として扱う
  • 監視そのものの停止には、週次レポートとトリガー失敗通知で備える
  • cronのジョブは、既存の行の先頭にラッパーを挟むだけで監視に乗せられる

業務自動化のご相談

「作った自動化が止まっていないか不安」「既存のスクリプトに監視の仕組みを入れたい」などのご相談を受け付けています。

🌐 https://datarou.com

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?