Files
pulse/server/dnsResolver.js
T
courtmanr@gmail.com bd9a976100 feat: enhance system reliability with DNS resilience and advanced alerting
- Implement resilient DNS resolution for API calls with fallback mechanisms
- Add compound threshold alert rules with multi-metric evaluation
- Enable immediate alert evaluation when rules are enabled
- Add alert rules file watching for dynamic configuration updates
- Improve alert state management with proper resolution handling
- Include comprehensive testing and documentation for DNS features

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-07 15:46:04 +01:00

146 lines
4.4 KiB
JavaScript

const dns = require('dns').promises;
const { promisify } = require('util');
const lookup = promisify(require('dns').lookup);
// Cache for DNS resolutions
const dnsCache = new Map();
const DNS_CACHE_TTL = 60000; // 1 minute cache
/**
* Custom DNS resolver with caching and round-robin support
*/
class DnsResolver {
constructor() {
this.failedHosts = new Map(); // Track failed hosts with timestamps
this.FAILED_HOST_RETRY_DELAY = 30000; // 30 seconds before retrying a failed host
}
/**
* Clear DNS cache
*/
clearCache() {
dnsCache.clear();
this.failedHosts.clear();
}
/**
* Mark a host/IP as failed
*/
markHostFailed(hostOrIp) {
console.log(`[DnsResolver] Marking host as failed: ${hostOrIp}`);
this.failedHosts.set(hostOrIp, Date.now());
}
/**
* Check if a host is marked as failed and still in the retry delay period
*/
isHostFailed(hostOrIp) {
const failedTime = this.failedHosts.get(hostOrIp);
if (!failedTime) return false;
const elapsed = Date.now() - failedTime;
if (elapsed > this.FAILED_HOST_RETRY_DELAY) {
// Remove from failed list after delay period
this.failedHosts.delete(hostOrIp);
return false;
}
return true;
}
/**
* Resolve hostname to IP addresses with caching
* @param {string} hostname - The hostname to resolve
* @returns {Promise<Array>} - Array of IP addresses
*/
async resolveHostname(hostname) {
// Check cache first
const cached = dnsCache.get(hostname);
if (cached && (Date.now() - cached.timestamp < DNS_CACHE_TTL)) {
console.log(`[DnsResolver] Using cached DNS resolution for ${hostname}: ${cached.addresses.length} addresses`);
return cached.addresses;
}
try {
// Try DNS resolution with both IPv4 and IPv6
const addresses = await dns.resolve4(hostname).catch(() => []);
const addresses6 = await dns.resolve6(hostname).catch(() => []);
const allAddresses = [...addresses, ...addresses6];
if (allAddresses.length === 0) {
// Fallback to lookup (uses system resolver)
console.log(`[DnsResolver] DNS resolve failed for ${hostname}, trying system lookup`);
const result = await lookup(hostname, { all: true });
const lookupAddresses = result.map(r => r.address);
if (lookupAddresses.length > 0) {
dnsCache.set(hostname, { addresses: lookupAddresses, timestamp: Date.now() });
console.log(`[DnsResolver] System lookup resolved ${hostname} to: ${lookupAddresses.join(', ')}`);
return lookupAddresses;
}
throw new Error(`No IP addresses found for ${hostname}`);
}
// Cache the results
dnsCache.set(hostname, { addresses: allAddresses, timestamp: Date.now() });
console.log(`[DnsResolver] Resolved ${hostname} to: ${allAddresses.join(', ')}`);
// Filter out failed IPs
const workingAddresses = allAddresses.filter(ip => !this.isHostFailed(ip));
if (workingAddresses.length === 0) {
console.warn(`[DnsResolver] All ${allAddresses.length} IPs for ${hostname} are marked as failed, using all anyway`);
return allAddresses;
}
return workingAddresses;
} catch (error) {
console.error(`[DnsResolver] Failed to resolve ${hostname}: ${error.message}`);
// Check if we have a stale cache entry we can use
const staleCache = dnsCache.get(hostname);
if (staleCache) {
console.warn(`[DnsResolver] Using stale DNS cache for ${hostname} due to resolution failure`);
return staleCache.addresses;
}
throw error;
}
}
/**
* Extract hostname from a URL or host:port string
*/
extractHostname(hostString) {
try {
if (hostString.includes('://')) {
const url = new URL(hostString);
return url.hostname;
} else {
// Handle host:port format
const parts = hostString.split(':');
return parts[0];
}
} catch (error) {
return hostString; // Return as-is if parsing fails
}
}
/**
* Test if a hostname can be resolved
*/
async canResolve(hostname) {
try {
const addresses = await this.resolveHostname(hostname);
return addresses.length > 0;
} catch (error) {
return false;
}
}
}
// Export singleton instance
module.exports = new DnsResolver();