Selenium Grid 监控实战指南:基于 Prometheus Exporter 与 Grafana 仪表盘的完整观测方案
Selenium Grid 监控实战指南:基于 Prometheus Exporter 与 Grafana 仪表盘的完整观测方案
导读
本文以 docker-selenium 仓库的 .monitoring 模块为骨架,系统讲解如何为 Selenium Grid 搭建完整的可观测体系:通过一个自定义 Prometheus Exporter 以单次 GraphQL 请求采集网格的容量、节点、会话与队列指标,再用 5 张预置 Grafana 仪表盘做可视化。读完本文,你将掌握 exporter 的构建、运行参数、指标语义与 PromQL 查询技巧,以及 Docker Compose 和 Kubernetes/Helm 两种部署形态下的完整配置方案,可直接照搬到自己的 Selenium Grid 环境中。
监控模块总览
仓库根目录下的 .monitoring/README.md 将该模块划分为两个核心部分:
| 目录 | 说明 |
|---|---|
| exporter/ | 自定义 Prometheus Exporter —— 构建、运行参数、指标参考、PromQL 示例 |
| dashboards/ | Grafana 仪表盘 JSON 文件 —— 面向 Docker Compose 与 Kubernetes/Helm 的配置供给 |
模块实际目录结构如下:
.monitoring/
├── README.md # 模块总览
├── exporter/ # Go 实现的 Prometheus Exporter
│ ├── main.go # 入口:flag 解析、HTTP server 装配
│ ├── graphql.go # GraphQL 查询构建、响应解码、时间解析
│ ├── collector.go # Prometheus collector 指标采集逻辑
│ └── *_test.go # 单元测试
├── dashboards/ # 5 张 Grafana 仪表盘 JSON
│ ├── selenium-grid-overview.json
│ ├── selenium-node-health.json
│ ├── selenium-sessions.json
│ ├── selenium-queue-capacity.json
│ └── selenium-cross-browser.json
└── config/ # 开箱即用的监控配置
├── prometheus.yml # Prometheus 抓取配置
└── grafana/provisioning/ # Grafana 数据源与仪表盘供给配置
其工作原理一句话概括:Exporter 周期性查询 Selenium Grid 的 GraphQL 端点,把返回的 JSON 转换成 Prometheus 指标格式,供 Prometheus 抓取;Grafana 仪表盘再消费这些指标完成可视化与告警。
Exporter 核心原理:一次抓取 = 一次 GraphQL 请求
与许多需要多次调用 REST API 的采集器不同,该 exporter 在 graphql.go 中定义了一个覆盖全部指标的复合查询,每次抓取仅发起一次 GraphQL POST 请求,同时拿到四类数据:
{
grid {
uri
totalSlots
nodeCount
maxSession
sessionCount
sessionQueueSize
version
}
nodesInfo {
nodes {
id
uri
status
maxSession
slotCount
sessionCount
stereotypes
version
osInfo { arch name version }
}
}
sessionsInfo {
sessionQueueRequests
sessions {
id
capabilities
startTime
sessionDurationMillis
nodeId
nodeUri
}
}
}
一次往返即可覆盖网格摘要(grid)、节点及其槽位(nodesInfo)、活动会话与队列(sessionsInfo)全部维度。对 GraphQL 响应的错误处理也很严谨:HTTP 请求失败、响应体解码失败、GraphQL 层返回 errors、或 data 为空,都会被识别并反映到 selenium_grid_scrape_success 指标上(见 graphql.go)。
值得注意的一个实现细节:Grid 返回的 startTime 是 DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss").withZone(ZoneId.systemDefault()) 格式的本地时间字符串,不带时区偏移。因此 exporter 需要用户通过 -grid-timezone 显式告知 Grid JVM 的时区,才能用 ParseInLocation 正确解析为 Unix 时间戳。仓库测试 graphql_test.go 覆盖了这类时间解析逻辑,保证跨时区部署下会话起止时间依然准确。
构建 Exporter
通过 Makefile 构建
从仓库根目录执行:
make build_exporter
产物位于 bin/selenium-grid-exporter。该目标定义在 Makefile:它会先将模块的 go 指令与本地 Go 工具链对齐(go mod edit -go=...),执行 go mod tidy,再以 -ldflags="-s -w" 去掉调试符号、压缩二进制体积。
直接使用 Go 构建
要求 Go 1.26+ 以及一个可达的 Selenium Grid 4+ 实例(GraphQL 端点需可访问):
cd .monitoring/exporter
go build -ldflags="-s -w" -o ../../bin/selenium-grid-exporter .
运行单元测试
每次升级 Go 工具链或依赖后,可用仓库提供的测试目标验证回归:
make test_exporter
该目标执行 go vet 与带 -race、-covermode=atomic 的全量单元测试(见 Makefile),与 CI 中的 go-test 工作流保持一致。
运行与命令行参数
构建完成后即可启动 exporter:
bin/selenium-grid-exporter \
-grid-url http://selenium-hub:4444/graphql \
-listen-address :9615 \
-scrape-timeout 10s \
-metrics-path /metrics \
-grid-timezone Asia/Ho_Chi_Minh \
-retain-stopped 5m
Flags 完整参考
| Flag | 默认值 | 说明 |
|---|---|---|
-grid-url |
$SE_SERVER_PROTOCOL://localhost:$SE_ROUTER_PORT/$SE_SUB_PATH/graphql |
Selenium Grid GraphQL 端点;自动从 Hub/Router 的环境变量拼装 |
-listen-address |
:9615 |
Exporter 监听地址 |
-scrape-timeout |
10s |
单次 GraphQL 抓取的 HTTP 超时 |
-metrics-path |
/metrics |
暴露指标的 URL 路径 |
-grid-timezone |
$TZ 或 UTC |
Grid JVM 的时区(用于解析 startTime),如 Asia/Ho_Chi_Minh |
-retain-stopped |
5m |
会话结束后,start/stop 指标保留多长时间 |
-username |
$SE_ROUTER_USERNAME |
访问 Grid 端点的 Basic Auth 用户名 |
-password |
$SE_ROUTER_PASSWORD |
访问 Grid 端点的 Basic Auth 密码 |
这些 flag 的解析逻辑位于 main.go:所有默认值都优先从 Hub/Router 容器环境变量推导。其中 -grid-url 的默认值由 defaultEndpoint() 函数动态拼装——读取 SE_SERVER_PROTOCOL(缺省 http)、SE_ROUTER_PORT(缺省 4444)和 SE_SUB_PATH,并对子路径做了归一化处理(空串、/、/selenium、/selenium/ 都会被规整为不带尾部斜杠的干净前缀),保证在自定义子路径部署时依然能正确命中 GraphQL 端点。
-grid-timezone 在启动时通过 time.LoadLocation 校验,非法时区会导致进程直接报错退出(newServer 返回错误),避免带病运行产生错误的会话时间指标。
部署形态
Docker Compose:独立容器
在 Compose 编排中,exporter 作为独立容器运行,通过网络访问 Hub 或 Router:
selenium-exporter:
image: selenium/selenium-grid-exporter:latest
environment:
SE_ROUTER_USERNAME: admin
SE_ROUTER_PASSWORD: secret
command:
- -grid-url=http://selenium-hub:4444/graphql
- -grid-timezone=UTC
ports:
- "9615:9615"
仓库还提供了开箱即用的抓取配置 .monitoring/config/prometheus.yml,默认 15 秒抓取一次,同时抓取 selenium-hub:9615 与 selenium-router:9615 两个目标;若 Grid 启用了 Basic Auth,只需取消注释 basic_auth 段:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: selenium_grid
static_configs:
- targets:
- "selenium-hub:9615"
- "selenium-router:9615"
# Uncomment if the Grid is protected by Basic Auth:
# basic_auth:
# username: admin
# password: secret
Kubernetes:内置到 Hub/Router 的边车模式
当 Helm 值 monitoring.enabled: true 时,exporter 二进制会被嵌入 Hub/Router 镜像,随 Selenium server 进程一同在 9615 端口自动启动,并自动创建 ServiceMonitor 让 Prometheus Operator 发现与抓取。相关默认值见 charts/selenium-grid/values.yaml,核心配置项如下:
monitoring:
enabled: false
enabledWithExistingAgent: false
exporter:
port: 9615 # Hub/Router 容器内 exporter 监听端口
grafana:
dashboards:
enabled: true # 为 Grafana 供给仪表盘 ConfigMap(依赖 sidecar)
serviceMonitor:
enabled: true # 生成 ServiceMonitor 资源
path: /metrics
interval: 30s
scrapeTimeout: 10s
podMonitor:
enabled: true # 也支持 PodMonitor 作为替代方案
path: /metrics
interval: 30s
Prometheus Operator 抓取
在 Helm values 中显式启用 ServiceMonitor 即可:
monitoring:
enabled: true
serviceMonitor:
enabled: true
Metrics 完整参考
Exporter 暴露的指标分为 6 大类,以下全部来自 collector.go 的实际定义。
Exporter 健康
| Metric | 类型 | 说明 |
|---|---|---|
selenium_grid_scrape_success |
Gauge | 最近一次 GraphQL 抓取成功为 1,失败(网络错误、鉴权错误等)为 0 |
selenium_grid_scrape_duration_seconds |
Gauge | 最近一次 GraphQL 抓取耗时(秒) |
这两个指标无论抓取成败都会输出,是告警 exporter 与 Grid 连通性的基础(见 collector.go 的 Collect 方法)。
网格摘要
| Metric | 类型 | Labels | 说明 |
|---|---|---|---|
selenium_grid_info |
Gauge | version |
恒为 1;Grid 版本字符串以 label 形式暴露 |
selenium_grid_total_slots |
Gauge | — | 所有节点的槽位总数 |
selenium_grid_node_count |
Gauge | — | 已注册节点数 |
selenium_grid_max_sessions |
Gauge | — | 全网格最大并发会话数 |
selenium_grid_session_count |
Gauge | — | 当前活动会话数 |
selenium_grid_session_queue_size |
Gauge | — | 队列中等待的会话请求数 |
节点
| Metric | 类型 | Labels | 说明 |
|---|---|---|---|
selenium_grid_node_status |
Gauge | node_id, uri, version, os_name, os_arch, os_version |
节点可用性:1=UP,0.5=DRAINING,0=DOWN |
selenium_grid_node_status_duration_seconds |
Gauge | node_id, status |
节点持续处于当前状态的秒数 |
selenium_grid_node_max_sessions |
Gauge | node_id |
节点最大并发会话数 |
selenium_grid_node_slot_count |
Gauge | node_id |
节点槽位总数 |
selenium_grid_node_session_count |
Gauge | node_id |
节点活动会话数 |
状态映射由 nodeStatusScore 完成(UP:1、DRAINING:0.5、DOWN:0)。collector 会为每个节点维护状态变更时间戳:只有状态真正变化时才重置计时,从而让 status_duration_seconds 准确反映节点在某一状态下的连续驻留时长;当节点从 Grid 注销后,其记录也会被清理(见 collector.go 的 collectNodes)。
节点 stereotype(槽位能力组合)
按节点每个槽位配置的能力组合拆解可用槽位数:
| Metric | 类型 | Labels | 说明 |
|---|---|---|---|
selenium_grid_node_stereotype_slots_total |
Gauge | node_id, browser_name, browser_version, platform_name |
每类 stereotype 能力的槽位数 |
示例 —— 某节点注册了 2 个 Chrome 槽位与 1 个 Firefox 槽位:
selenium_grid_node_stereotype_slots_total{node_id="abc",browser_name="chrome",browser_version="124",platform_name="linux"} 2
selenium_grid_node_stereotype_slots_total{node_id="abc",browser_name="firefox",browser_version="125",platform_name="linux"} 1
注意 GraphQL 返回的 stereotypes 字段是 JSON 编码字符串,exporter 会通过 parseStereotypes 解码后逐条展开为独立序列(见 collector.go 与 graphql.go)。
活动会话
| Metric | 类型 | Labels | 说明 |
|---|---|---|---|
selenium_grid_sessions_active |
Gauge | browser_name, browser_version, platform_name |
按能力聚合的活动会话数 |
selenium_grid_session_duration_seconds |
Gauge | session_id, node_id, browser_name, browser_version, platform_name, test_name, container_name |
每个活动会话的持续时长(秒) |
时长优先使用 Grid 上报的 sessionDurationMillis;当 Grid 尚未上报该值时,会回退为「当前时间 − 会话启动时间」的墙钟计算,保证指标始终有值(见 collector.go 的 collectSessions)。
会话生命周期(start / stop 时间)
| Metric | 类型 | Labels | 说明 |
|---|---|---|---|
selenium_grid_session_start_seconds |
Gauge | session_id, node_id, browser_name, browser_version, platform_name, test_name, container_name |
会话启动的 Unix 时间戳(解析自 Grid 的 startTime 字段) |
selenium_grid_session_stop_seconds |
Gauge | session_id, node_id, browser_name, browser_version, platform_name, test_name, container_name |
会话结束的 Unix 时间戳(会话终止后的首次抓取时设置) |
selenium_grid_sessions_completed_total |
Counter | — | 自 exporter 启动以来结束的会话总数(exporter 重启后归零) |
工作机制:
session_start_seconds在每次抓取时对所有活动会话输出;session_stop_seconds在 exporter 检测到某会话从 Grid 响应中消失时设置,并在-retain-stopped(默认5m)窗口内持续保留,确保 Prometheus 至少抓取到一次;sessions_completed_total在每次会话消失时递增,可配合rate()/increase()计算吞吐量;-grid-timezone必须与 Grid 服务器的 JVM 时区一致,因为 Grid 将startTime格式化为不带偏移的本地时间字符串。
会话队列
| Metric | 类型 | Labels | 说明 |
|---|---|---|---|
selenium_grid_session_queue_requests |
Gauge | browser_name, browser_version, platform_name |
按期望能力聚合的排队请求数 |
输出示例
一个运行中网格的典型 /metrics 输出:
# HELP selenium_grid_total_slots Total number of slots across all nodes.
# TYPE selenium_grid_total_slots gauge
selenium_grid_total_slots 8
# HELP selenium_grid_node_count Number of registered nodes.
# TYPE selenium_grid_node_count gauge
selenium_grid_node_count 2
# HELP selenium_grid_session_count Number of active sessions grid-wide.
# TYPE selenium_grid_session_count gauge
selenium_grid_session_count 3
# HELP selenium_grid_session_queue_size Number of session requests waiting in the queue.
# TYPE selenium_grid_session_queue_size gauge
selenium_grid_session_queue_size 1
# HELP selenium_grid_node_status Node availability: 1=UP, 0.5=DRAINING, 0=DOWN.
# TYPE selenium_grid_node_status gauge
selenium_grid_node_status{node_id="abc",uri="http://node1:5555",version="4.20.0",os_name="Linux",os_arch="amd64",os_version="5.15"} 1
selenium_grid_node_status{node_id="def",uri="http://node2:5555",version="4.20.0",os_name="Linux",os_arch="amd64",os_version="5.15"} 0.5
# HELP selenium_grid_node_stereotype_slots_total Slots available per node stereotype (browser/version/platform combination).
# TYPE selenium_grid_node_stereotype_slots_total gauge
selenium_grid_node_stereotype_slots_total{node_id="abc",browser_name="chrome",browser_version="124",platform_name="linux"} 4
selenium_grid_node_stereotype_slots_total{node_id="def",browser_name="firefox",browser_version="125",platform_name="linux"} 4
# HELP selenium_grid_sessions_active Number of active sessions by capability.
# TYPE selenium_grid_sessions_active gauge
selenium_grid_sessions_active{browser_name="chrome",browser_version="124",platform_name="linux"} 2
selenium_grid_sessions_active{browser_name="firefox",browser_version="125",platform_name="linux"} 1
# HELP selenium_grid_session_duration_seconds Duration of an active session in seconds.
# TYPE selenium_grid_session_duration_seconds gauge
selenium_grid_session_duration_seconds{session_id="s1",node_id="abc",browser_name="chrome",browser_version="124",platform_name="linux"} 42.3
# HELP selenium_grid_session_queue_requests Number of queued session requests by desired capability.
# TYPE selenium_grid_session_queue_requests gauge
selenium_grid_session_queue_requests{browser_name="chrome",browser_version="124",platform_name="linux"} 1
实用 PromQL 查询
容量与负载
排队会话 vs 总容量:
selenium_grid_session_queue_size / selenium_grid_total_slots
按浏览器统计槽位利用率:
selenium_grid_sessions_active
/ on(browser_name, browser_version, platform_name)
sum by(browser_name, browser_version, platform_name) (selenium_grid_node_stereotype_slots_total)
节点不在 UP 状态:
selenium_grid_node_status < 1
DRAINING 超过 2 分钟的节点:
selenium_grid_node_status_duration_seconds{status="DRAINING"} > 120
每个节点在当前状态的持续时长:
selenium_grid_node_status_duration_seconds
会话追踪
所有活动会话的当前时长:
selenium_grid_session_duration_seconds
某个测试的全部会话:
selenium_grid_session_duration_seconds{test_name="my-login-test"}
某个容器中的所有会话:
selenium_grid_session_duration_seconds{container_name="node-chrome-1"}
指定会话的启动时间:
selenium_grid_session_start_seconds{session_id="<id>"}
指定会话的结束时间(结束后的 -retain-stopped 窗口内可用):
selenium_grid_session_stop_seconds{session_id="<id>"}
已完成会话的总墙钟时长(start → stop):
selenium_grid_session_stop_seconds{session_id="<id>"}
- selenium_grid_session_start_seconds{session_id="<id>"}
运行超过 10 分钟的所有会话:
(selenium_grid_session_stop_seconds - selenium_grid_session_start_seconds) > 600
连通性与吞吐
告警:exporter 无法到达 Grid:
selenium_grid_scrape_success == 0
会话吞吐(5 分钟窗口内的每秒完成数):
rate(selenium_grid_sessions_completed_total[5m])
最近一小时内完成的会话数:
increase(selenium_grid_sessions_completed_total[1h])
Grid 版本:
selenium_grid_info
Grafana 仪表盘
.monitoring/dashboards/README.md 提供了 5 张预置仪表盘,均消费上述 exporter 指标:
| 文件 | UID | 用途 |
|---|---|---|
selenium-grid-overview.json |
sel-grid-overview |
网格健康一览:槽位、会话、队列、节点汇总表 |
selenium-node-health.json |
sel-node-health |
每节点状态、容量、stereotype 分解、状态时长趋势 |
selenium-sessions.json |
sel-sessions |
按浏览器/平台的会话分布、带测试名与容器名的会话明细表、已完成会话历史 |
selenium-queue-capacity.json |
sel-queue-cap |
队列深度、槽位利用压力、按期望能力拆分的队列 |
selenium-cross-browser.json |
sel-cross-browser |
跨浏览器测试覆盖:各浏览器活动数、槽位利用率、会话趋势、队列压力与版本分布 |
所有仪表盘默认每 30 秒自动刷新(Queue & Capacity 为 15 秒)。
数据源要求
仪表盘按 UID prometheus 引用 Prometheus 数据源。这是 kube-prometheus-stack 与标准 Grafana Docker Compose 环境中的默认 UID;若你的数据源 UID 不同,需在 Grafana 的 Connections → Data sources 中更新。
仓库在 .monitoring/config/grafana/provisioning/datasources/prometheus.yml 提供了配套的数据源供给配置,其 UID 为 selenium-prometheus、指向 http://prometheus:9090,若直接使用该配置则无需手动改 UID。
Docker Compose 供给
将仪表盘目录挂载到 Grafana 的 provisioning 路径,并添加供给配置:
grafana:
volumes:
- ./.monitoring/dashboards:/var/lib/grafana/dashboards/selenium
- ./.monitoring/config/grafana/provisioning:/etc/grafana/provisioning
provisioning/dashboards/selenium.yaml(仓库对应文件见 .monitoring/config/grafana/provisioning/dashboards/selenium.yml,每 30 秒刷新一次供给):
apiVersion: 1
providers:
- name: Selenium Grid
folder: Selenium Grid
type: file
options:
path: /var/lib/grafana/dashboards/selenium
Kubernetes(Helm chart)供给
当 monitoring.enabled: true 时,Helm chart 会为每张仪表盘自动创建带 grafana_dashboard: "1" 标签的 ConfigMap。Grafana 的 sidecar 容器检测到这些标签后,会将仪表盘供给到 Selenium Grid 文件夹:
monitoring:
enabled: true
grafana:
dashboards:
enabled: true # monitoring.enabled=true 时的默认值
Grafana sidecar 必须配置为监控所有命名空间(kube-prometheus-stack 默认行为):
kube-prometheus-stack:
grafana:
sidecar:
dashboards:
searchNamespace: ALL
仪表盘更新流程
若需在 Grafana UI 中调整仪表盘后同步回仓库,官方推荐流程:
- 在 Grafana UI 中编辑仪表盘;
- 通过 Share → Export → Save to file 导出(勾选 Export for sharing externally 以包含
__inputs); - 用导出的文件替换本目录对应 JSON;
- 若数据源输入被导出为
${DS_PROMETHEUS},需将每次出现替换为字面量prometheus,并删除__inputs与__requires块 —— provisioning 不处理__inputs,只有 UI 导入器才处理; - 执行
make copy_dashboards将更新后的文件同步进 Helm chart(该目标被 Makefile 中的chart_build/chart_build_nightly前置依赖,确保镜像构建前仪表盘始终同步)。
小结
至此,一条完整的 Selenium Grid 观测链路已经清晰:Go 编写的 exporter 用单次 GraphQL 请求抓取网格全貌 → 输出 6 大类 20 余个语义明确的 Prometheus 指标 → Prometheus(或 Prometheus Operator)按配置周期抓取 → 5 张预置 Grafana 仪表盘实时可视化。无论是 Docker Compose 的独立容器形态,还是 Kubernetes 下嵌入 Hub/Router 的边车形态,仓库都给出了可直接复用的配置与源码实现;配合文中整理的全量指标表与 PromQL 模板,你可以快速定位节点排空、槽位耗尽、会话积压与跨浏览器覆盖不足等典型问题。