{
  "name": "SLO 服務等級監控",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 * * * *"
            }
          ]
        }
      },
      "id": "schedule",
      "name": "每小時執行",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [240, 300]
    },
    {
      "parameters": {
        "url": "https://mo.wooo.work/health",
        "options": { "timeout": 10000 }
      },
      "id": "uat-check",
      "name": "UAT 可用性",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [460, 200],
      "continueOnFail": true
    },
    {
      "parameters": {
        "url": "https://momo.wooo.work/health",
        "options": { "timeout": 10000 }
      },
      "id": "gcp-check",
      "name": "GCP 可用性",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [460, 400],
      "continueOnFail": true
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineAll",
        "options": {}
      },
      "id": "merge",
      "name": "合併結果",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [680, 300]
    },
    {
      "parameters": {
        "jsCode": "const results = $input.all();\nconst now = new Date();\nconst hour = now.getHours();\nconst dateKey = now.toISOString().slice(0, 10);\n\n// 從 static data 讀取 SLO 數據\nconst sloData = $getWorkflowStaticData('global');\nif (!sloData.metrics) {\n  sloData.metrics = {\n    uat: { checks: 0, success: 0, latency: [] },\n    gcp: { checks: 0, success: 0, latency: [] },\n    daily: {}\n  };\n}\n\n// UAT 結果\nconst uatResult = results[0]?.json;\nconst uatSuccess = uatResult && uatResult.status === 'healthy';\nconst uatLatency = results[0]?.json?.$responseTime || 0;\n\nsloData.metrics.uat.checks++;\nif (uatSuccess) sloData.metrics.uat.success++;\nif (uatLatency > 0) {\n  sloData.metrics.uat.latency.push(uatLatency);\n  if (sloData.metrics.uat.latency.length > 168) { // 保留 7 天\n    sloData.metrics.uat.latency.shift();\n  }\n}\n\n// GCP 結果\nconst gcpResult = results[1]?.json;\nconst gcpSuccess = gcpResult && gcpResult.status === 'healthy';\nconst gcpLatency = results[1]?.json?.$responseTime || 0;\n\nsloData.metrics.gcp.checks++;\nif (gcpSuccess) sloData.metrics.gcp.success++;\nif (gcpLatency > 0) {\n  sloData.metrics.gcp.latency.push(gcpLatency);\n  if (sloData.metrics.gcp.latency.length > 168) {\n    sloData.metrics.gcp.latency.shift();\n  }\n}\n\n// 計算 SLO 指標\nconst uatAvailability = (sloData.metrics.uat.success / sloData.metrics.uat.checks * 100).toFixed(2);\nconst gcpAvailability = (sloData.metrics.gcp.success / sloData.metrics.gcp.checks * 100).toFixed(2);\n\nconst avgUatLatency = sloData.metrics.uat.latency.length > 0 \n  ? (sloData.metrics.uat.latency.reduce((a, b) => a + b, 0) / sloData.metrics.uat.latency.length).toFixed(0)\n  : 0;\nconst avgGcpLatency = sloData.metrics.gcp.latency.length > 0\n  ? (sloData.metrics.gcp.latency.reduce((a, b) => a + b, 0) / sloData.metrics.gcp.latency.length).toFixed(0)\n  : 0;\n\n// SLO 目標\nconst SLO_AVAILABILITY = 99.5;  // 可用性目標 99.5%\nconst SLO_LATENCY = 3000;        // 延遲目標 3 秒內\n\nconst uatMeetsSLO = parseFloat(uatAvailability) >= SLO_AVAILABILITY;\nconst gcpMeetsSLO = parseFloat(gcpAvailability) >= SLO_AVAILABILITY;\n\n// 每日報告（每天 09:00）\nconst shouldReport = hour === 9;\n\nreturn [{\n  json: {\n    uat: {\n      currentStatus: uatSuccess ? 'healthy' : 'unhealthy',\n      availability: uatAvailability + '%',\n      avgLatency: avgUatLatency + 'ms',\n      meetsSLO: uatMeetsSLO,\n      totalChecks: sloData.metrics.uat.checks\n    },\n    gcp: {\n      currentStatus: gcpSuccess ? 'healthy' : 'unhealthy',\n      availability: gcpAvailability + '%',\n      avgLatency: avgGcpLatency + 'ms',\n      meetsSLO: gcpMeetsSLO,\n      totalChecks: sloData.metrics.gcp.checks\n    },\n    sloTarget: {\n      availability: SLO_AVAILABILITY + '%',\n      latency: SLO_LATENCY + 'ms'\n    },\n    shouldReport,\n    timestamp: now.toISOString()\n  }\n}];"
      },
      "id": "calculate-slo",
      "name": "計算 SLO 指標",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [900, 300]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "should-report",
              "leftValue": "={{ $json.shouldReport }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-report",
      "name": "每日報告?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [1120, 300]
    },
    {
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst now = new Date().toLocaleString('zh-TW', { timeZone: 'Asia/Taipei' });\n\nlet message = `📊 <b>每日 SLO 報告</b>\\n\\n`;\nmessage += `<b>服務等級目標</b>\\n`;\nmessage += `├ 可用性目標: ${data.sloTarget.availability}\\n`;\nmessage += `└ 延遲目標: < ${data.sloTarget.latency}\\n\\n`;\n\nmessage += `<b>🟦 UAT 環境</b>\\n`;\nmessage += `├ 可用性: ${data.uat.availability} ${data.uat.meetsSLO ? '✅' : '❌'}\\n`;\nmessage += `├ 平均延遲: ${data.uat.avgLatency}\\n`;\nmessage += `└ 檢查次數: ${data.uat.totalChecks}\\n\\n`;\n\nmessage += `<b>🟥 GCP 環境</b>\\n`;\nmessage += `├ 可用性: ${data.gcp.availability} ${data.gcp.meetsSLO ? '✅' : '❌'}\\n`;\nmessage += `├ 平均延遲: ${data.gcp.avgLatency}\\n`;\nmessage += `└ 檢查次數: ${data.gcp.totalChecks}\\n\\n`;\n\nmessage += `⏰ 報告時間: ${now}`;\n\nreturn [{ json: { message } }];"
      },
      "id": "format-report",
      "name": "格式化報告",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1340, 200]
    },
    {
      "parameters": {
        "url": "https://api.telegram.org/bot8075645931:AAH-EGKMo8ZC4QJs-Nc1_0s92xHrGdQvdpg/sendMessage",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"chat_id\": \"5619078117\",\n  \"parse_mode\": \"HTML\",\n  \"text\": \"{{ $json.message }}\"\n}",
        "options": {}
      },
      "id": "telegram",
      "name": "發送報告",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1560, 200]
    }
  ],
  "connections": {
    "每小時執行": {
      "main": [
        [
          { "node": "UAT 可用性", "type": "main", "index": 0 },
          { "node": "GCP 可用性", "type": "main", "index": 0 }
        ]
      ]
    },
    "UAT 可用性": {
      "main": [
        [{ "node": "合併結果", "type": "main", "index": 0 }]
      ]
    },
    "GCP 可用性": {
      "main": [
        [{ "node": "合併結果", "type": "main", "index": 1 }]
      ]
    },
    "合併結果": {
      "main": [
        [{ "node": "計算 SLO 指標", "type": "main", "index": 0 }]
      ]
    },
    "計算 SLO 指標": {
      "main": [
        [{ "node": "每日報告?", "type": "main", "index": 0 }]
      ]
    },
    "每日報告?": {
      "main": [
        [{ "node": "格式化報告", "type": "main", "index": 0 }],
        []
      ]
    },
    "格式化報告": {
      "main": [
        [{ "node": "發送報告", "type": "main", "index": 0 }]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": [],
  "triggerCount": 0,
  "updatedAt": "2026-02-07T17:00:00.000Z",
  "versionId": "1"
}
