const express = require('express'); const { checkKeycloakToken } = require('./keycloakCheck'); const { initialState, evaluate } = require('./alertState'); const { sendAlert } = require('./notifier'); const app = express(); const PORT = process.env.PORT || 3000; const config = { keycloakBaseUrl: process.env.KEYCLOAK_BASE_URL, keycloakRealm: process.env.KEYCLOAK_REALM, keycloakClientId: process.env.KEYCLOAK_CLIENT_ID, keycloakClientSecret: process.env.KEYCLOAK_CLIENT_SECRET, checkIntervalMs: Number(process.env.CHECK_INTERVAL_SECONDS || 60) * 1000, alertThreshold: Number(process.env.ALERT_THRESHOLD || 3), realertIntervalMs: Number(process.env.REALERT_INTERVAL_MINUTES || 30) * 60 * 1000, }; let alertState = initialState(); let lastCheck = { ok: null, at: null, error: null }; async function tick() { const result = await checkKeycloakToken({ baseUrl: config.keycloakBaseUrl, realm: config.keycloakRealm, clientId: config.keycloakClientId, clientSecret: config.keycloakClientSecret, }); lastCheck = { ok: result.ok, at: new Date().toISOString(), error: result.error || null }; const { newState, action } = evaluate(alertState, result.ok, Date.now(), { alertThreshold: config.alertThreshold, realertIntervalMs: config.realertIntervalMs, }); alertState = newState; const target = `${config.keycloakBaseUrl}/realms/${config.keycloakRealm}`; if (action === 'alert-down') { console.error(`[ALERT] Keycloak異常検知(${config.alertThreshold}回連続失敗): ${result.error}`); await sendAlert({ subject: '[緊急] Keycloak SSO 異常検知', text: [ `Keycloakのトークン発行が${config.alertThreshold}回連続で失敗しました。`, '', `エラー: ${result.error}`, `発生時刻: ${lastCheck.at}`, `監視対象: ${target}`, '', 'プリザンター・XWiki等のSSOログインに影響している可能性があります。', ].join('\n'), }).catch((e) => console.error('通知メール送信失敗:', e.message)); } else if (action === 'alert-still-down') { console.error(`[ALERT] Keycloak異常継続中: ${result.error}`); await sendAlert({ subject: '[継続] Keycloak SSO 異常継続中', text: [ 'Keycloakのトークン発行が引き続き失敗しています。', '', `エラー: ${result.error}`, `確認時刻: ${lastCheck.at}`, `監視対象: ${target}`, ].join('\n'), }).catch((e) => console.error('通知メール送信失敗:', e.message)); } else if (action === 'alert-recovered') { console.log('[RECOVER] Keycloak復旧確認'); await sendAlert({ subject: '[復旧] Keycloak SSO 復旧', text: [ 'Keycloakのトークン発行が復旧しました。', '', `復旧確認時刻: ${lastCheck.at}`, `監視対象: ${target}`, ].join('\n'), }).catch((e) => console.error('通知メール送信失敗:', e.message)); } } app.get('/', (req, res) => { res.json({ app: 'keycloak-monitor', status: 'ok' }); }); // Dokploy/Traefikのヘルスチェックが叩くエンドポイント。 // 監視プロセス自身の生存確認であり、Keycloakの状態そのものではない(それは/status参照)。 app.get('/health', (req, res) => { res.status(200).json({ status: 'healthy' }); }); app.get('/status', (req, res) => { res.json({ lastCheck, consecutiveFailures: alertState.consecutiveFailures, alerted: alertState.alerted, checkIntervalSeconds: config.checkIntervalMs / 1000, alertThreshold: config.alertThreshold, realertIntervalMinutes: config.realertIntervalMs / 60000, notifyEmails: (process.env.NOTIFY_EMAILS || '').split(',').map((s) => s.trim()).filter(Boolean), }); }); /* c8 ignore start */ if (require.main === module) { app.listen(PORT, () => { console.log(`keycloak-monitor listening on port ${PORT}`); if (!config.keycloakClientSecret) { console.warn('KEYCLOAK_CLIENT_SECRET未設定です。監視は失敗し続けます。'); } tick(); setInterval(tick, config.checkIntervalMs); }); } /* c8 ignore stop */ module.exports = { app, tick };