1、新增功能探针联动处置、心跳在线检测

2、syslog-consumer模块拆分 syslog-consumer-rule模块实现日志数据消费、解析、泛化入库。
This commit is contained in:
2026-05-28 14:30:06 +08:00
parent 19c563b3f3
commit a360895292
1479 changed files with 116572 additions and 4549 deletions
@@ -0,0 +1,176 @@
package com.haobang.interlocking;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* 探针联动API客户端
* 用于调用syslog-consumer模块提供的REST API
*/
@Component
public class InterlockingApiClient {
private static final Logger logger = LoggerFactory.getLogger(InterlockingApiClient.class);
private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();
@Value("${interlocking.api-key:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6}")
private String apiKey;
@Value("${interlocking.api.base-url:http://localhost:8089/xdrservice/interlocking}")
private String baseUrl;
/**
* 获取待执行的封禁指令
* @param probeId 探针ID
* @return 指令列表
*/
public List<Map<String, Object>> getPendingCommands(Long probeId) {
String url = baseUrl + "/cmd/pending?probeId=" + probeId;
try {
String response = doGet(url);
JsonNode jsonNode = objectMapper.readTree(response);
if (jsonNode.has("code") && jsonNode.get("code").asInt() == 200) {
JsonNode dataNode = jsonNode.get("data");
List<Map<String, Object>> commands = new ArrayList<>();
if (dataNode.isArray()) {
for (JsonNode node : dataNode) {
commands.add(objectMapper.convertValue(node, Map.class));
}
}
return commands;
}
logger.warn("获取待执行指令失败: {}", response);
return Collections.emptyList();
} catch (Exception e) {
logger.error("获取待执行指令异常: {}", e.getMessage(), e);
return Collections.emptyList();
}
}
/**
* 更新指令状态为执行中
* @param cmdId 指令ID
* @return 是否成功
*/
public boolean updateStatusToExecuting(Long cmdId) {
String url = baseUrl + "/cmd/" + cmdId + "/status/executing";
try {
String response = doPut(url, null);
JsonNode jsonNode = objectMapper.readTree(response);
return jsonNode.has("code") && jsonNode.get("code").asInt() == 200;
} catch (Exception e) {
logger.error("更新指令状态为执行中失败: cmdId={}, error={}", cmdId, e.getMessage());
return false;
}
}
/**
* 更新指令状态为执行完成
* @param cmdId 指令ID
* @return 是否成功
*/
public boolean updateStatusToCompleted(Long cmdId) {
String url = baseUrl + "/cmd/" + cmdId + "/status/completed";
try {
String response = doPut(url, null);
JsonNode jsonNode = objectMapper.readTree(response);
return jsonNode.has("code") && jsonNode.get("code").asInt() == 200;
} catch (Exception e) {
logger.error("更新指令状态为执行完成失败: cmdId={}, error={}", cmdId, e.getMessage());
return false;
}
}
/**
* 更新指令状态为执行失败
* @param cmdId 指令ID
* @param errorMessage 错误信息
* @return 是否成功
*/
public boolean updateStatusToFailed(Long cmdId, String errorMessage) {
String url = baseUrl + "/cmd/" + cmdId + "/status/failed";
try {
Map<String, Object> body = new HashMap<>();
body.put("errorMessage", errorMessage);
String response = doPut(url, body);
JsonNode jsonNode = objectMapper.readTree(response);
return jsonNode.has("code") && jsonNode.get("code").asInt() == 200;
} catch (Exception e) {
logger.error("更新指令状态为执行失败失败: cmdId={}, error={}", cmdId, e.getMessage());
return false;
}
}
/**
* 批量插入封禁记录
* @param logs 封禁记录列表
* @return 是否成功
*/
public boolean batchInsertLogs(List<Map<String, Object>> logs) {
String url = baseUrl + "/log/batch";
try {
Map<String, Object> request = new HashMap<>();
request.put("logs", logs);
String response = doPost(url, request);
JsonNode jsonNode = objectMapper.readTree(response);
return jsonNode.has("code") && jsonNode.get("code").asInt() == 200;
} catch (Exception e) {
logger.error("批量插入封禁记录失败: error={}", e.getMessage());
return false;
}
}
/**
* 执行GET请求
*/
private String doGet(String url) {
HttpHeaders headers = new HttpHeaders();
headers.set("X-API-KEY", apiKey);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
return response.getBody();
}
/**
* 执行PUT请求
*/
private String doPut(String url, Map<String, Object> body) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("X-API-KEY", apiKey);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.PUT, entity, String.class);
return response.getBody();
}
/**
* 执行POST请求
*/
private String doPost(String url, Map<String, Object> body) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("X-API-KEY", apiKey);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
return response.getBody();
}
public String getApiKey() { return apiKey; }
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
public String getBaseUrl() { return baseUrl; }
public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
}
@@ -0,0 +1,412 @@
package com.haobang.interlocking;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.haobang.firewall.FirewallApiClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 探针联动封禁服务
* 核心业务逻辑:
* 1. 从syslog-consumer获取待执行的封禁指令
* 2. 解析封禁指令,生成逐条指令
* 3. 调用防火墙API执行封禁
* 4. 记录封禁结果到syslog-consumer
* 5. 更新指令状态
*/
@Service
public class InterlockingService {
private static final Logger logger = LoggerFactory.getLogger(InterlockingService.class);
@Autowired
private InterlockingApiClient apiClient;
@Autowired
private FirewallApiClient firewallClient;
private final ObjectMapper objectMapper = new ObjectMapper();
// 探针ID,从配置读取
@Value("${app.service.device_collect_id:1}")
private Integer probeId;
// 是否启用联动功能
@Value("${interlocking.enabled:true}")
private boolean interlockingEnabled;
// 执行锁,防止重复执行
private final AtomicBoolean executing = new AtomicBoolean(false);
@PostConstruct
public void init() {
logger.info("探针联动封禁服务初始化完成,探针ID: {}, 启用状态: {}", probeId, interlockingEnabled);
}
/**
* 定时任务:检查并执行待处理的封禁指令
* 每30秒执行一次
*/
@Scheduled(fixedDelay = 30000)
public void processPendingCommands() {
if (!interlockingEnabled) {
return;
}
// 防止重复执行
if (!executing.compareAndSet(false, true)) {
logger.debug("上次执行尚未完成,跳过本次调度");
return;
}
try {
logger.info("开始检查待执行的封禁指令...");
// 1. 获取待执行的封禁指令
List<Map<String, Object>> pendingCommands = apiClient.getPendingCommands(probeId.longValue());
if (pendingCommands.isEmpty()) {
logger.debug("没有待执行的封禁指令");
return;
}
logger.info("获取到 {} 条待执行的封禁指令", pendingCommands.size());
// 2. 逐条处理指令
for (Map<String, Object> cmd : pendingCommands) {
try {
processCommand(cmd);
} catch (Exception e) {
logger.error("处理封禁指令异常: cmdId={}, error={}", cmd.get("id"), e.getMessage(), e);
}
}
} catch (Exception e) {
logger.error("检查待执行指令失败: {}", e.getMessage(), e);
} finally {
executing.set(false);
}
}
/**
* 处理单条封禁指令
* 指令数据结构(JSON字段名为驼峰格式):
* - banIps: 封禁IP数组
* - banType: 封禁类型(0:黑名单、1:白名单)
* - banOperationType: 操作类型(0:新增、1:删除)
* - banDuration: 封禁时长(秒)
* - deviceInterlockingIp: 联动设备IP数组
* - deviceInterlockingId: 联动设备ID数组
*/
private void processCommand(Map<String, Object> cmd) {
Long cmdId = ((Number) cmd.get("id")).longValue();
// JSON字段名使用驼峰格式
List<String> banIps = parseStringArray(cmd.get("banIps"));
String banType = (String) cmd.get("banType");
Integer banOperationType = cmd.get("banOperationType") != null ?
((Number) cmd.get("banOperationType")).intValue() : 0;
Integer banDuration = cmd.get("banDuration") != null ?
((Number) cmd.get("banDuration")).intValue() : -1;
List<String> deviceIps = parseStringArray(cmd.get("deviceInterlockingIp"));
logger.info("开始处理封禁指令: cmdId={}, banType={}, operationType={}, ipCount={}",
cmdId, banType, banOperationType, banIps.size());
try {
// 3. 更新状态为执行中
if (!apiClient.updateStatusToExecuting(cmdId)) {
logger.warn("更新指令状态为执行中失败,跳过此指令: cmdId={}", cmdId);
return;
}
if (banIps.isEmpty()) {
logger.warn("封禁IP列表为空: cmdId={}", cmdId);
apiClient.updateStatusToCompleted(cmdId);
return;
}
// 4. 根据设备列表执行封禁
int successCount = 0;
int failCount = 0;
List<Map<String, Object>> allResults = new ArrayList<>();
// 遍历每个联动设备执行封禁
for (String deviceIp : deviceIps) {
// 设置防火墙客户端的设备IP(这里简化处理,实际可能需要从配置获取)
List<Map<String, Object>> results = executeBlockOperations(
deviceIp, banType, banOperationType, banIps, banDuration, cmdId);
allResults.addAll(results);
for (Map<String, Object> result : results) {
// banResult: 1表示成功,0表示失败
if ("1".equals(result.get("banResult"))) {
successCount++;
} else {
failCount++;
}
}
}
// 5. 批量记录封禁结果
for (Map<String, Object> result : allResults) {
result.put("deviceInterlockingCmdId", cmdId);
result.put("probeId", probeId);
result.put("banMethod", "1"); // 自动化封禁
}
boolean logSaved = apiClient.batchInsertLogs(allResults);
if (!logSaved) {
logger.warn("批量记录封禁结果失败: cmdId={}", cmdId);
}
// 6. 更新指令状态为执行完成
if (apiClient.updateStatusToCompleted(cmdId)) {
logger.info("封禁指令执行完成: cmdId={}, 成功={}, 失败={}", cmdId, successCount, failCount);
} else {
logger.error("更新指令状态为执行完成失败: cmdId={}", cmdId);
}
} catch (Exception e) {
logger.error("处理封禁指令异常: cmdId={}, error={}", cmdId, e.getMessage(), e);
apiClient.updateStatusToFailed(cmdId, e.getMessage());
}
}
/**
* 解析PostgreSQL数组格式
*/
private List<String> parseStringArray(Object obj) {
List<String> result = new ArrayList<>();
if (obj == null) {
return result;
}
try {
if (obj instanceof List) {
for (Object item : (List<?>) obj) {
result.add(String.valueOf(item));
}
} else if (obj instanceof String) {
String str = (String) obj;
// 处理PostgreSQL数组格式 {a,b,c}
if (str.startsWith("{") && str.endsWith("}")) {
str = str.substring(1, str.length() - 1);
for (String item : str.split(",")) {
if (!item.isEmpty()) {
result.add(item.trim());
}
}
}
}
} catch (Exception e) {
logger.error("解析数组失败: {}", e.getMessage());
}
return result;
}
/**
* 执行封禁操作
* @param deviceIp 设备IP
* @param banType 封禁类型(0:黑名单、1:白名单)
* @param operationType 操作类型(0:新增、1:删除)
* @param ipList IP列表
* @param duration 封禁时长
* @param cmdId 指令ID
*/
private List<Map<String, Object>> executeBlockOperations(
String deviceIp, String banType, int operationType,
List<String> ipList, int duration, Long cmdId) {
List<Map<String, Object>> results = new ArrayList<>();
// 确定操作类型
String operationName;
if ("0".equals(banType)) {
// 黑名单
operationName = operationType == 0 ? "add_blacklist" : "del_blacklist";
} else {
// 白名单
operationName = operationType == 0 ? "add_whitelist" : "del_whitelist";
}
for (String ip : ipList) {
Map<String, Object> result = new HashMap<>();
result.put("banIp", ip);
result.put("deviceInterlockingId", null); // 后续可关联
result.put("deviceName", deviceIp);
result.put("banResult", "0"); // 默认失败,0表示失败
result.put("reqBody", "");
result.put("respBody", "");
try {
FirewallApiClient.FirewallResponse response;
switch (operationName) {
case "add_blacklist":
response = firewallClient.addToBlacklist(ip, duration, "联动封禁");
break;
case "del_blacklist":
response = firewallClient.removeFromBlacklist(ip);
break;
case "add_whitelist":
response = firewallClient.addToWhitelist(ip, "whitelist_" + ip, "联动封禁白名单");
break;
case "del_whitelist":
response = firewallClient.removeFromWhitelist(ip);
break;
default:
logger.warn("未知的指令类型: {}", operationName);
result.put("respBody", "未知的指令类型: " + operationName);
results.add(result);
continue;
}
// banResult: 1表示成功,0表示失败
result.put("banResult", response.isSuccess() ? "1" : "0");
result.put("respBody", response.getRawResponse());
// 构建请求body用于记录
result.put("reqBody", buildRequestBody(operationName, ip, duration));
} catch (Exception e) {
logger.error("执行封禁操作异常: ip={}, operation={}, error={}", ip, operationName, e.getMessage());
result.put("respBody", "执行异常: " + e.getMessage());
}
results.add(result);
}
return results;
}
/**
* 构建请求body用于记录
*/
private String buildRequestBody(String cmdType, String ip, int duration) {
try {
Map<String, Object> body = new HashMap<>();
switch (cmdType) {
case "add_blacklist":
body.put("blist", ip);
body.put("age", String.valueOf(duration));
body.put("enable", "1");
break;
case "del_blacklist":
body.put("blist", ip);
break;
case "add_whitelist":
body.put("enable", "1");
body.put("name", "whitelist_" + ip);
List<Map<String, String>> addr = new ArrayList<>();
Map<String, String> addrItem = new HashMap<>();
addrItem.put("address", ip);
addr.add(addrItem);
body.put("addr", addr);
break;
case "del_whitelist":
body.put("name", ip);
break;
}
return objectMapper.writeValueAsString(body);
} catch (Exception e) {
return "";
}
}
/**
* 手动触发封禁
*/
public Map<String, Object> manualBlock(String ip, String cmdType, int age, String reason) {
Map<String, Object> result = new HashMap<>();
try {
FirewallApiClient.FirewallResponse response;
switch (cmdType) {
case "add_blacklist":
response = firewallClient.addToBlacklist(ip, age, reason);
break;
case "del_blacklist":
response = firewallClient.removeFromBlacklist(ip);
break;
case "add_whitelist":
response = firewallClient.addToWhitelist(ip, "manual_" + ip, reason);
break;
case "del_whitelist":
response = firewallClient.removeFromWhitelist(ip);
break;
default:
result.put("success", false);
result.put("message", "未知的指令类型: " + cmdType);
return result;
}
result.put("success", response.isSuccess());
result.put("code", response.getCode());
result.put("message", response.getMessage());
} catch (Exception e) {
logger.error("手动封禁异常: ip={}, cmdType={}, error={}", ip, cmdType, e.getMessage());
result.put("success", false);
result.put("message", "执行异常: " + e.getMessage());
}
return result;
}
/**
* 批量手动封禁
*/
public Map<String, Object> batchManualBlock(List<String> ips, String cmdType, int age, String reason) {
Map<String, Object> result = new HashMap<>();
List<Map<String, String>> ipList = new ArrayList<>();
for (String ip : ips) {
Map<String, String> ipInfo = new HashMap<>();
ipInfo.put("ip", ip);
ipInfo.put("age", String.valueOf(age));
ipInfo.put("reason", reason);
ipList.add(ipInfo);
}
try {
FirewallApiClient.FirewallResponse response;
if ("add_blacklist".equals(cmdType)) {
response = firewallClient.batchAddToBlacklist(ipList);
} else if ("del_blacklist".equals(cmdType)) {
response = firewallClient.batchRemoveFromBlacklist(ips);
} else {
result.put("success", false);
result.put("message", "批量操作暂不支持此类型: " + cmdType);
return result;
}
result.put("success", response.isSuccess());
result.put("code", response.getCode());
result.put("message", response.getMessage());
} catch (Exception e) {
logger.error("批量手动封禁异常: cmdType={}, error={}", cmdType, e.getMessage());
result.put("success", false);
result.put("message", "执行异常: " + e.getMessage());
}
return result;
}
public Integer getProbeId() { return probeId; }
public void setProbeId(Integer probeId) { this.probeId = probeId; }
public boolean isInterlockingEnabled() { return interlockingEnabled; }
public void setInterlockingEnabled(boolean interlockingEnabled) { this.interlockingEnabled = interlockingEnabled; }
}
@@ -0,0 +1,239 @@
package com.haobang.interlocking;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 探针心跳客户端
* 定期向平台端发送心跳,支持重试和指数退避
*/
@Component
public class ProbeHeartbeatClient {
private static final Logger logger = LoggerFactory.getLogger(ProbeHeartbeatClient.class);
private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper();
@Value("${probe.heartbeat.enabled:true}")
private boolean heartbeatEnabled;
@Value("${probe.heartbeat.interval-seconds:60}")
private int heartbeatIntervalSeconds;
@Value("${app.service.device_collect_id:1}")
private String collectId;
@Value("${app.service.device_collect_name:采集探针-01}")
private String collectName;
@Value("${app.service.version:V1.0.0-20260509}")
private String appVersion;
@Value("${probe.platform.api-url:http://localhost:8089/xdrservice/interlocking/probe/heartbeat}")
private String platformApiUrl;
@Value("${interlocking.api-key:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6}")
private String apiKey;
/** 重试次数 */
private final AtomicInteger retryCount = new AtomicInteger(0);
/** 最大重试次数 */
private static final int MAX_RETRIES = 5;
/** 初始重试延迟(毫秒) */
private static final long INITIAL_BACKOFF_MS = 1000;
/** 最大退避延迟(毫秒) */
private static final long MAX_BACKOFF_MS = 60000;
/**
* 发送心跳
* @return 是否成功
*/
public boolean sendHeartbeat() {
if (!heartbeatEnabled) {
logger.debug("心跳发送已禁用");
return false;
}
// 构建心跳数据
Map<String, Object> heartbeatData = buildHeartbeatData();
try {
// 发送HTTP POST请求
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("X-API-KEY", apiKey);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(heartbeatData, headers);
ResponseEntity<String> response = restTemplate.exchange(
platformApiUrl,
HttpMethod.POST,
entity,
String.class
);
if (response.getStatusCode() == HttpStatus.OK) {
// 重置重试计数
retryCount.set(0);
// 检查响应
String body = response.getBody();
if (body != null && body.contains("\"code\":200")) {
logger.debug("心跳发送成功: collectId={}, timestamp={}", collectId, heartbeatData.get("timestamp"));
return true;
}
}
// 请求失败,准备重试
handleFailure("HTTP " + response.getStatusCodeValue());
return false;
} catch (Exception e) {
handleFailure(e.getMessage());
return false;
}
}
/**
* 发送心跳(简化版,供其他服务调用)
*/
public boolean sendHeartbeat(String alertType, String alertContent) {
// 直接发送普通心跳
return sendHeartbeat();
}
/**
* 构建心跳数据
*/
private Map<String, Object> buildHeartbeatData() {
Map<String, Object> data = new HashMap<>();
// 使用配置的collectId或自动获取
String probeId = collectId;
if (probeId == null || probeId.isEmpty()) {
probeId = getLocalHostIdentifier();
}
data.put("collectId", probeId);
data.put("collectName", collectName != null && !collectName.isEmpty() ? collectName : "SyslogServe");
data.put("deviceIp", getLocalIp());
data.put("appVersion", appVersion);
data.put("timestamp", LocalDateTime.now().toString());
// 添加负载状态(可选)
Map<String, Object> loadStatus = new HashMap<>();
loadStatus.put("memoryUsage", getMemoryUsage());
loadStatus.put("threadCount", Thread.activeCount());
data.put("loadStatus", toJson(loadStatus));
return data;
}
/**
* 获取本地主机标识符
*/
private String getLocalHostIdentifier() {
try {
InetAddress ip = InetAddress.getLocalHost();
return ip.getHostName() + "-" + ip.getHostAddress();
} catch (UnknownHostException e) {
return "unknown-" + System.currentTimeMillis();
}
}
/**
* 获取本地IP地址
*/
private String getLocalIp() {
try {
InetAddress ip = InetAddress.getLocalHost();
return ip.getHostAddress();
} catch (UnknownHostException e) {
return "127.0.0.1";
}
}
/**
* 获取内存使用情况
*/
private double getMemoryUsage() {
Runtime runtime = Runtime.getRuntime();
long totalMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();
long usedMemory = totalMemory - freeMemory;
return Math.round((double) usedMemory / totalMemory * 100 * 100) / 100.0;
}
/**
* 对象转JSON字符串
*/
private String toJson(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (Exception e) {
return "{}";
}
}
/**
* 处理发送失败,使用指数退避重试
*/
private void handleFailure(String errorMsg) {
int currentRetry = retryCount.incrementAndGet();
if (currentRetry <= MAX_RETRIES) {
// 计算退避延迟
long backoffMs = Math.min(INITIAL_BACKOFF_MS * (1L << (currentRetry - 1)), MAX_BACKOFF_MS);
logger.warn("心跳发送失败 (尝试 {}/{}): {}, {}ms后将重试",
currentRetry, MAX_RETRIES, errorMsg, backoffMs);
try {
Thread.sleep(backoffMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else {
logger.error("心跳发送失败,已达到最大重试次数 {}: {}", MAX_RETRIES, errorMsg);
// 重置重试计数,下次将重新开始
retryCount.set(0);
}
}
/**
* 获取配置的心跳间隔
*/
public int getHeartbeatIntervalSeconds() {
return heartbeatIntervalSeconds;
}
/**
* 获取探针ID
*/
public String getCollectId() {
return collectId != null && !collectId.isEmpty() ? collectId : getLocalHostIdentifier();
}
/**
* 检查心跳功能是否启用
*/
public boolean isEnabled() {
return heartbeatEnabled;
}
}