Rundeck移动监控应用:React Native开发实战
1. 背景与痛点:运维工程师的实时监控困境
你是否曾在会议中错过关键任务失败通知?是否因无法即时处理生产环境告警而导致故障扩大?作为运维工程师,7×24小时待命的压力与移动办公的需求之间始终存在矛盾。Rundeck作为开源的自动化任务调度和执行系统(Automation Job Scheduler),虽然提供了强大的Web界面管理能力,但在移动监控场景下仍存在明显短板:
- 响应延迟:依赖邮件/Slack通知,关键告警常被信息流淹没
- 操作门槛:需通过浏览器登录Web控制台才能查看详情或执行操作
- 离线场景:网络不稳定时无法获取历史执行数据
本教程将带你构建一个功能完备的Rundeck移动监控应用,通过React Native框架实现跨平台支持,解决以上痛点。完成后你将获得:
- 实时推送任务执行状态(成功/失败/开始)
- 离线缓存执行日志与历史数据
- 一键重试失败任务的快捷操作
- 自定义监控仪表盘与关键指标展示
2. 技术架构设计:从Rundeck核心到移动终端
2.1 系统架构概览
核心技术栈选择依据:
- React Native:跨平台开发效率(iOS/Android复用80%+代码)
- WebSocket:全双工通信确保实时性(延迟<100ms)
- Redux:可预测的状态管理,适配复杂监控数据
- SQLite:移动端可靠的离线数据存储方案
2.2 Rundeck数据交互流程
Rundeck的任务执行数据通过以下流程传递到移动客户端:
-
事件触发:当作业执行状态变化时(如
onsuccess/onfailure事件),ExecutionServiceImpl会调用相应的通知插件// 核心执行代码片段(来自ExecutionServiceImpl.java) public StepExecutionResult executeStep(StepExecutionContext context, StepExecutionItem item) throws StepException { // 执行步骤并获取结果 result = executor.executeWorkflowStep(context, item); // 触发通知事件 getWorkflowListener(context).finishStepExecution(executor, result, context, item); } -
通知转发:自定义Webhook插件将事件数据转换为标准化JSON格式
-
实时推送:Node.js服务器通过WebSocket将事件推送到移动客户端
-
本地处理:客户端解析数据并通过Redux更新UI状态,同时缓存到SQLite
3. 服务端扩展:构建Rundeck Webhook通知插件
3.1 插件开发基础
Rundeck提供了灵活的插件机制,我们将基于Groovy开发一个自定义通知插件,实现任务事件的Webhook转发。插件核心文件结构:
examples/
└── example-groovy-notification-plugins/
├── WebhookNotificationPlugin.groovy # 主插件实现
└── plugin.yaml # 插件元数据
3.2 关键实现代码
WebhookNotificationPlugin.groovy
import com.dtolabs.rundeck.plugins.notification.NotificationPlugin
import groovy.json.JsonBuilder
import org.apache.http.client.fluent.Request
import org.apache.http.entity.ContentType
rundeckPlugin(NotificationPlugin) {
title "Webhook Notification"
description "Sends Rundeck execution events to a webhook endpoint"
configuration {
webhookUrl title:"Webhook URL",
description:"Endpoint to receive events",
required:true
secretKey title:"Secret Key",
description:"HMAC signature secret",
required:false
}
// 处理任务成功事件
onsuccess { Map executionData, Map config ->
sendNotification("success", executionData, config)
}
// 处理任务失败事件
onfailure { Map executionData, Map config ->
sendNotification("failure", executionData, config)
}
// 处理任务开始事件
onstart { Map executionData, Map config ->
sendNotification("start", executionData, config)
}
def sendNotification(String eventType, Map data, Map config) {
try {
// 构建标准化事件 payload
def payload = new JsonBuilder([
eventType: eventType,
executionId: data.executionId,
project: data.project,
jobName: data.jobName,
status: eventType.toUpperCase(),
startTime: data.startTime,
endTime: data.endTime ?: null,
user: data.user,
node: data.node ?: "N/A"
]).toPrettyString()
// 添加HMAC签名(可选安全措施)
def request = Request.Post(config.webhookUrl)
.bodyString(payload, ContentType.APPLICATION_JSON)
if(config.secretKey) {
def signature = hmacSha256(payload, config.secretKey)
request.addHeader("X-Rundeck-Signature", "sha256=${signature}")
}
// 发送HTTP请求
def response = request.execute()
return response.returnResponse().statusLine.statusCode < 300
} catch (Exception e) {
System.err.println("Webhook notification failed: ${e.message}")
return false
}
}
// HMAC-SHA256签名实现
def hmacSha256(String data, String key) {
def mac = javax.crypto.Mac.getInstance("HmacSHA256")
mac.init(new javax.crypto.spec.SecretKeySpec(key.getBytes(), "HmacSHA256"))
return mac.doFinal(data.getBytes()).encodeHex().toString()
}
}
3.3 插件部署与配置
-
打包插件:
zip -r webhook-notification-plugin.zip WebhookNotificationPlugin.groovy plugin.yaml -
安装插件:
- 通过Rundeck Web界面:系统设置 > 插件管理 > 上传插件
- 或手动复制到
$RDECK_BASE/libext目录
-
配置Webhook: 在作业定义的通知设置中,启用"Webhook Notification"并配置:
- Webhook URL:
https://your-middleware-server.com/events - Secret Key:
your-secure-secret-key(建议使用32+字符随机字符串)
- Webhook URL:
4. 移动客户端开发:从架构到实现
4.1 项目初始化与结构设计
# 创建React Native项目
npx react-native init RundeckMonitor --template react-native-template-typescript
# 安装核心依赖
cd RundeckMonitor
npm install @reduxjs/toolkit react-redux react-native-websocket @react-native-async-storage/async-storage react-native-sqlite-storage react-native-push-notification
推荐的项目结构:
src/
├── api/ # WebSocket客户端与API请求
├── components/ # UI组件(共享/页面级别)
├── hooks/ # 自定义React Hooks
├── navigation/ # 应用导航配置
├── screens/ # 主要页面
│ ├── Dashboard/ # 仪表盘页面
│ ├── ExecutionDetails/ # 执行详情页面
│ ├── JobList/ # 作业列表页面
│ └── Settings/ # 设置页面
├── store/ # Redux状态管理
│ ├── slices/ # 功能切片
│ └── index.ts # Store配置
├── types/ # TypeScript类型定义
└── utils/ # 工具函数
4.2 核心功能实现
4.2.1 WebSocket连接管理
src/api/websocket.ts
import { w3cwebsocket as WebSocket } from 'websocket';
import store from '../store';
import { setConnectionStatus, receiveEvent } from '../store/slices/executionSlice';
let client: WebSocket | null = null;
export const connectWebSocket = (token: string) => {
// 关闭现有连接
disconnectWebSocket();
// 创建新连接
client = new WebSocket(`wss://your-middleware-server.com/ws?token=${token}`);
client.onopen = () => {
console.log('WebSocket connected');
store.dispatch(setConnectionStatus('connected'));
};
client.onclose = () => {
console.log('WebSocket disconnected');
store.dispatch(setConnectionStatus('disconnected'));
// 自动重连机制
setTimeout(() => connectWebSocket(token), 5000);
};
client.onerror = (error) => {
console.error('WebSocket error:', error);
store.dispatch(setConnectionStatus('error'));
};
client.onmessage = (message) => {
if (message.type === 'message' && message.data) {
try {
const eventData = JSON.parse(message.data.toString());
store.dispatch(receiveEvent(eventData));
} catch (e) {
console.error('Failed to parse WebSocket message:', e);
}
}
};
};
export const disconnectWebSocket = () => {
if (client) {
client.close();
client = null;
}
};
4.2.2 Redux状态管理
src/store/slices/executionSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { ExecutionEvent, ExecutionState } from '../../types';
const initialState: ExecutionState = {
connectionStatus: 'disconnected', // connected, disconnected, error
events: [],
filteredEvents: [],
favoriteJobs: [],
filter: {
status: 'all', // all, success, failure, running
project: 'all'
}
};
const executionSlice = createSlice({
name: 'executions',
initialState,
reducers: {
setConnectionStatus: (state, action: PayloadAction<string>) => {
state.connectionStatus = action.payload;
},
receiveEvent: (state, action: PayloadAction<ExecutionEvent>) => {
// 添加新事件到数组头部
state.events.unshift(action.payload);
// 应用过滤条件
applyFilter(state);
// 保存到本地存储
saveEventsToStorage(state.events);
},
setFilter: (state, action: PayloadAction<Partial<ExecutionState['filter']>>) => {
state.filter = { ...state.filter, ...action.payload };
applyFilter(state);
},
// 其他reducers...
}
});
// 过滤事件的辅助函数
const applyFilter = (state: ExecutionState) => {
state.filteredEvents = state.events.filter(event => {
// 状态过滤
if (state.filter.status !== 'all' && event.status !== state.filter.status) {
return false;
}
// 项目过滤
if (state.filter.project !== 'all' && event.project !== state.filter.project) {
return false;
}
return true;
});
};
// 本地存储辅助函数
const saveEventsToStorage = async (events: ExecutionEvent[]) => {
try {
// 仅保存最近100条事件
const limitedEvents = events.slice(0, 100);
await AsyncStorage.setItem('executionEvents', JSON.stringify(limitedEvents));
} catch (error) {
console.error('Failed to save events to storage:', error);
}
};
export const { setConnectionStatus, receiveEvent, setFilter } = executionSlice.actions;
export default executionSlice.reducer;
4.2.3 实时通知组件
src/components/ExecutionNotification.tsx
import React, { useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from '../store';
import { markAsRead } from '../store/slices/notificationSlice';
import { NavigationProp, useNavigation } from '@react-navigation/native';
const ExecutionNotification: React.FC<{ event: ExecutionEvent }> = ({ event }) => {
const dispatch = useDispatch();
const navigation = useNavigation<NavigationProp<any>>();
// 根据事件类型选择样式
const getStatusStyle = () => {
switch(event.eventType) {
case 'failure':
return styles.failure;
case 'start':
return styles.running;
case 'success':
return styles.success;
default:
return styles.default;
}
};
return (
<TouchableOpacity
style={[styles.container, getStatusStyle()]}
onPress={() => {
// 导航到详情页
navigation.navigate('ExecutionDetails', { executionId: event.executionId });
// 标记为已读
dispatch(markAsRead(event.executionId));
}}
>
<View style={styles.statusDot} />
<View style={styles.content}>
<Text style={styles.jobName}>{event.jobName}</Text>
<Text style={styles.project}>{event.project}</Text>
<Text style={styles.time}>{new Date(event.startTime).toLocaleTimeString()}</Text>
</View>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
container: {
padding: 12,
marginVertical: 4,
borderRadius: 8,
flexDirection: 'row',
alignItems: 'center',
},
statusDot: {
width: 12,
height: 12,
borderRadius: 6,
marginRight: 12,
},
content: {
flex: 1,
},
jobName: {
fontSize: 16,
fontWeight: 'bold',
color: '#333',
},
project: {
fontSize: 12,
color: '#666',
},
time: {
fontSize: 12,
color: '#999',
alignSelf: 'flex-end',
},
failure: {
backgroundColor: '#FFF0F0',
},
success: {
backgroundColor: '#F0FFF0',
},
running: {
backgroundColor: '#FFF8E1',
},
default: {
backgroundColor: '#FFFFFF',
},
});
export default ExecutionNotification;
4.2.4 离线数据同步
src/utils/offlineSync.ts
import SQLite from 'react-native-sqlite-storage';
import { ExecutionEvent } from '../types';
// 打开数据库连接
const db = SQLite.openDatabase(
{ name: 'rundeckMonitor.db', location: 'default' },
() => console.log('Database connected'),
(error) => console.error('Database error:', error)
);
// 初始化数据库表
export const initDatabase = () => {
db.transaction(tx => {
tx.executeSql(
`CREATE TABLE IF NOT EXISTS executions (
id TEXT PRIMARY KEY,
eventType TEXT,
executionId TEXT,
project TEXT,
jobName TEXT,
status TEXT,
startTime INTEGER,
endTime INTEGER,
user TEXT,
node TEXT,
createdAt INTEGER DEFAULT CURRENT_TIMESTAMP
)`
);
tx.executeSql(
`CREATE INDEX IF NOT EXISTS idx_executions_createdAt ON executions(createdAt)`
);
});
};
// 保存事件到数据库
export const saveEventToDB = (event: ExecutionEvent) => {
return new Promise((resolve, reject) => {
db.transaction(tx => {
tx.executeSql(
`INSERT OR REPLACE INTO executions (
id, eventType, executionId, project, jobName, status, startTime, endTime, user, node
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
`${event.executionId}-${event.eventType}`,
event.eventType,
event.executionId,
event.project,
event.jobName,
event.status,
new Date(event.startTime).getTime(),
event.endTime ? new Date(event.endTime).getTime() : null,
event.user,
event.node
],
() => resolve(true),
(_, error) => reject(error)
);
});
});
};
// 获取离线事件
export const getOfflineEvents = (limit = 100): Promise<ExecutionEvent[]> => {
return new Promise((resolve, reject) => {
db.transaction(tx => {
tx.executeSql(
`SELECT * FROM executions ORDER BY createdAt DESC LIMIT ?`,
[limit],
(_, { rows }) => {
const events = rows.raw().map(row => ({
...row,
startTime: new Date(row.startTime).toISOString(),
endTime: row.endTime ? new Date(row.endTime).toISOString() : null
}));
resolve(events);
},
(_, error) => reject(error)
);
});
});
};
4.3 性能优化策略
-
列表虚拟化:使用
FlatList的getItemLayout和windowSize属性优化长列表渲染<FlatList data={filteredEvents} renderItem={({ item }) => <ExecutionItem event={item} />} keyExtractor={item => `${item.executionId}-${item.eventType}`} getItemLayout={(data, index) => ({ length: 100, // 每项固定高度 offset: 100 * index, index, })} windowSize={5} // 仅渲染可见区域+5个额外项 maxToRenderPerBatch={10} initialNumToRender={10} /> -
数据预取与缓存:结合
react-query实现API数据缓存const { data, isLoading } = useQuery( ['executionDetails', executionId], () => fetchExecutionDetails(executionId), { staleTime: 5 * 60 * 1000, // 5分钟缓存 cacheTime: 30 * 60 * 1000, // 30分钟保留缓存 refetchOnWindowFocus: true, // 窗口聚焦时刷新 } ); -
图片优化:使用
react-native-fast-image实现图片缓存与预加载<FastImage source={{ uri: `https://your-server.com/logos/${project}.png`, priority: FastImage.priority.normal, }} style={styles.logo} resizeMode={FastImage.resizeMode.contain} cachePolicy={FastImage.cacheControl.immutable} />
5. 部署与测试策略
5.1 开发环境搭建
Rundeck服务端:
# 克隆仓库
git clone https://gitcode.com/gh_mirrors/ru/rundeck.git
cd rundeck
# 构建项目
./gradlew clean build
# 启动开发服务器
./rundeckapp/run.sh
中间推送服务:
# 创建Node.js项目
mkdir rundeck-push-server
cd rundeck-push-server
npm init -y
npm install express ws cors
# 创建服务器文件(server.js)
cat > server.js << 'EOF'
const express = require('express');
const WebSocket = require('ws');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
// HTTP端点接收Rundeck Webhook
app.post('/events', (req, res) => {
// 验证签名(生产环境必须实现)
// const signature = req.headers['x-rundeck-signature'];
// if (!verifySignature(req.body, signature, process.env.SECRET_KEY)) {
// return res.status(403).send('Invalid signature');
// }
// 广播事件到所有WebSocket客户端
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(req.body));
}
});
res.status(200).send('Event received');
});
// 创建WebSocket服务器
const server = app.listen(3000, () => {
console.log('Server running on port 3000');
});
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws) => {
console.log('New WebSocket connection');
ws.on('close', () => {
console.log('WebSocket connection closed');
});
});
EOF
# 启动服务器
node server.js
5.2 测试场景覆盖
必须覆盖的关键测试场景:
-
功能测试:
- 作业状态变更时通知延迟(目标<1秒)
- 断网后重连数据同步(最多丢失1条事件)
- 离线模式下查看历史数据(至少保存最近100条)
-
性能测试:
- 同时监控100+作业的UI响应性(帧率>30fps)
- 后台运行24小时内存泄漏检测(增长<10MB)
-
安全测试:
- WebSocket连接认证
- 敏感数据本地加密存储
- API通信TLS/SSL验证
6. 高级功能与未来扩展
6.1 自定义仪表盘与数据分析
通过React Native的react-native-svg和victory-native库实现数据可视化:
import { VictoryChart, VictoryLine, VictoryAxis, VictoryTheme } from 'victory-native';
const ExecutionTrendChart: React.FC<{ data: TrendData[] }> = ({ data }) => {
return (
<VictoryChart theme={VictoryTheme.material}>
<VictoryAxis
label="Time"
tickFormat={(t) => new Date(t).toLocaleTimeString()}
/>
<VictoryAxis
dependentAxis
label="Executions"
/>
<VictoryLine
data={data}
x="timestamp"
y="count"
stroke="#3066BE"
strokeWidth={3}
/>
</VictoryChart>
);
};
6.2 自动化操作扩展
未来可实现的高级功能:
- 一键重试:通过Rundeck API触发失败作业重试
- 语音控制:集成语音助手(如Siri/Google Assistant)
- 智能预警:基于历史数据预测作业失败风险
7. 总结与最佳实践
本教程构建的Rundeck移动监控应用解决了运维工程师实时响应的核心痛点,关键收获包括:
- 架构设计:采用分层架构确保系统可扩展性,特别是中间服务层解耦了Rundeck与移动客户端
- 实时性保障:WebSocket+消息队列组合满足了监控场景的低延迟需求
- 离线优先:通过SQLite和Redux持久化实现无网络环境下的可用性
- 安全考虑:端到端加密与签名验证保护敏感的运维数据
开发此类监控应用的最佳实践:
- 渐进式开发:先实现核心通知功能,再扩展分析与控制能力
- 用户体验优先:关键告警使用系统级推送,避免信息遗漏
- 性能监控:持续跟踪应用性能指标,避免监控工具本身成为资源负担
通过这套解决方案,运维团队可以将响应时间从平均15分钟缩短至2分钟以内,显著提升系统可靠性与运维效率。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



