ken_nogi/NodeSrv/apps/keycloak-monitor/src/index.js
Kenichiro NOGI ce58cb4be4 初回コミット: dev配下(NodeSrv/Pleasanter等)をGitea管理下に統合
GitHub(nextgroup2706/ken_nogi)は今後使わず自社Gitea運用に切替え。
NodeSrvは旧リポジトリの履歴を破棄しファイルのみ統合(Dokploy用サービスアカウントは
別途mygit-admin/NodeSrv.gitに履歴あり)。notepmエクスポート(12GB)とPleasanter
インストーラzip(208MB)はサイズが大きいため.gitignoreで除外。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 15:37:06 +09:00

115 lines
4.1 KiB
JavaScript

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 };