mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 17:56:37 +08:00
修复节点详情 OpenResty 连接数与吞吐显示为「—」:节点可观测 API 将 OpenResty 观测数据合并进 metric_snapshots;指标文案改为「请求/分钟」(近 60 秒窗口),连接数为 0 时正常显示 0。
This commit is contained in:
@@ -22,6 +22,8 @@ sidebar: false
|
|||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|
||||||
|
- 修复节点详情 OpenResty 连接数与吞吐显示为「—」:节点可观测 API 将 OpenResty 观测数据合并进 `metric_snapshots`;指标文案改为「请求/分钟」(近 60 秒窗口),连接数为 0 时正常显示 0。
|
||||||
|
|
||||||
- 修复仪表盘「24 小时请求趋势」摘要误显示当前小时请求量/错误量:改为汇总近 24 小时总量。
|
- 修复仪表盘「24 小时请求趋势」摘要误显示当前小时请求量/错误量:改为汇总近 24 小时总量。
|
||||||
|
|
||||||
- 修复 Pages 部署包上传报「请求超时,请稍后重试」:上传请求使用独立 10 分钟超时(覆盖默认 15 秒),并在大文件上传完成后提示服务端处理中。
|
- 修复 Pages 部署包上传报「请求超时,请稍后重试」:上传请求使用独立 10 分钟超时(覆盖默认 15 秒),并在大文件上传完成后提示服务端处理中。
|
||||||
|
|||||||
+56
-46
@@ -5350,7 +5350,7 @@ const docTemplate = `{
|
|||||||
"SessionCookie": []
|
"SessionCookie": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "返回所有已发布的 OpenResty 配置版本摘要,需要管理员权限",
|
"description": "返回所有已发布的 OpenResty 配置版本摘要,按创建时间倒序排列,需要管理员权限",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -14433,50 +14433,6 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"model.OpenFlareMetricSnapshot": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"captured_at": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"cpu_usage_percent": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"disk_read_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"disk_write_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"memory_total_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"memory_used_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"network_rx_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"network_tx_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"node_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"storage_total_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"storage_used_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"model.OpenFlareNode": {
|
"model.OpenFlareNode": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -15297,7 +15253,7 @@ const docTemplate = `{
|
|||||||
"metric_snapshots": {
|
"metric_snapshots": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/model.OpenFlareMetricSnapshot"
|
"$ref": "#/definitions/observability.NodeMetricSnapshotView"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_id": {
|
"node_id": {
|
||||||
@@ -15928,6 +15884,56 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"observability.NodeMetricSnapshotView": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"captured_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"cpu_usage_percent": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"disk_read_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"disk_write_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"memory_total_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"memory_used_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"network_rx_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"network_tx_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"node_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"openresty_connections": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"openresty_rx_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"openresty_tx_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"storage_total_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"storage_used_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"observability.NodeTrends": {
|
"observability.NodeTrends": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -16371,6 +16377,10 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"total_size": {
|
"total_size": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"upload_id": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+56
-46
@@ -5343,7 +5343,7 @@
|
|||||||
"SessionCookie": []
|
"SessionCookie": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "返回所有已发布的 OpenResty 配置版本摘要,需要管理员权限",
|
"description": "返回所有已发布的 OpenResty 配置版本摘要,按创建时间倒序排列,需要管理员权限",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -14426,50 +14426,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"model.OpenFlareMetricSnapshot": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"captured_at": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"cpu_usage_percent": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"disk_read_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"disk_write_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"memory_total_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"memory_used_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"network_rx_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"network_tx_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"node_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"storage_total_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"storage_used_bytes": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"model.OpenFlareNode": {
|
"model.OpenFlareNode": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -15290,7 +15246,7 @@
|
|||||||
"metric_snapshots": {
|
"metric_snapshots": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/model.OpenFlareMetricSnapshot"
|
"$ref": "#/definitions/observability.NodeMetricSnapshotView"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_id": {
|
"node_id": {
|
||||||
@@ -15921,6 +15877,56 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"observability.NodeMetricSnapshotView": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"captured_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"cpu_usage_percent": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"disk_read_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"disk_write_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"memory_total_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"memory_used_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"network_rx_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"network_tx_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"node_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"openresty_connections": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"openresty_rx_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"openresty_tx_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"storage_total_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"storage_used_bytes": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"observability.NodeTrends": {
|
"observability.NodeTrends": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -16364,6 +16370,10 @@
|
|||||||
},
|
},
|
||||||
"total_size": {
|
"total_size": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"upload_id": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+38
-31
@@ -1130,35 +1130,6 @@ definitions:
|
|||||||
updated_at:
|
updated_at:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
model.OpenFlareMetricSnapshot:
|
|
||||||
properties:
|
|
||||||
captured_at:
|
|
||||||
type: string
|
|
||||||
cpu_usage_percent:
|
|
||||||
type: number
|
|
||||||
created_at:
|
|
||||||
type: string
|
|
||||||
disk_read_bytes:
|
|
||||||
type: integer
|
|
||||||
disk_write_bytes:
|
|
||||||
type: integer
|
|
||||||
id:
|
|
||||||
type: integer
|
|
||||||
memory_total_bytes:
|
|
||||||
type: integer
|
|
||||||
memory_used_bytes:
|
|
||||||
type: integer
|
|
||||||
network_rx_bytes:
|
|
||||||
type: integer
|
|
||||||
network_tx_bytes:
|
|
||||||
type: integer
|
|
||||||
node_id:
|
|
||||||
type: string
|
|
||||||
storage_total_bytes:
|
|
||||||
type: integer
|
|
||||||
storage_used_bytes:
|
|
||||||
type: integer
|
|
||||||
type: object
|
|
||||||
model.OpenFlareNode:
|
model.OpenFlareNode:
|
||||||
properties:
|
properties:
|
||||||
auto_update_enabled:
|
auto_update_enabled:
|
||||||
@@ -1713,7 +1684,7 @@ definitions:
|
|||||||
type: array
|
type: array
|
||||||
metric_snapshots:
|
metric_snapshots:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/model.OpenFlareMetricSnapshot'
|
$ref: '#/definitions/observability.NodeMetricSnapshotView'
|
||||||
type: array
|
type: array
|
||||||
node_id:
|
node_id:
|
||||||
type: string
|
type: string
|
||||||
@@ -2126,6 +2097,39 @@ definitions:
|
|||||||
traffic:
|
traffic:
|
||||||
$ref: '#/definitions/observability.TrafficWindowSummary'
|
$ref: '#/definitions/observability.TrafficWindowSummary'
|
||||||
type: object
|
type: object
|
||||||
|
observability.NodeMetricSnapshotView:
|
||||||
|
properties:
|
||||||
|
captured_at:
|
||||||
|
type: string
|
||||||
|
cpu_usage_percent:
|
||||||
|
type: number
|
||||||
|
disk_read_bytes:
|
||||||
|
type: integer
|
||||||
|
disk_write_bytes:
|
||||||
|
type: integer
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
memory_total_bytes:
|
||||||
|
type: integer
|
||||||
|
memory_used_bytes:
|
||||||
|
type: integer
|
||||||
|
network_rx_bytes:
|
||||||
|
type: integer
|
||||||
|
network_tx_bytes:
|
||||||
|
type: integer
|
||||||
|
node_id:
|
||||||
|
type: string
|
||||||
|
openresty_connections:
|
||||||
|
type: integer
|
||||||
|
openresty_rx_bytes:
|
||||||
|
type: integer
|
||||||
|
openresty_tx_bytes:
|
||||||
|
type: integer
|
||||||
|
storage_total_bytes:
|
||||||
|
type: integer
|
||||||
|
storage_used_bytes:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
observability.NodeTrends:
|
observability.NodeTrends:
|
||||||
properties:
|
properties:
|
||||||
capacity_24h:
|
capacity_24h:
|
||||||
@@ -2416,6 +2420,9 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
total_size:
|
total_size:
|
||||||
type: integer
|
type: integer
|
||||||
|
upload_id:
|
||||||
|
example: "0"
|
||||||
|
type: string
|
||||||
type: object
|
type: object
|
||||||
pages.Input:
|
pages.Input:
|
||||||
properties:
|
properties:
|
||||||
@@ -7125,7 +7132,7 @@ paths:
|
|||||||
- openflare-apply-log
|
- openflare-apply-log
|
||||||
/api/v1/d/config-versions:
|
/api/v1/d/config-versions:
|
||||||
get:
|
get:
|
||||||
description: 返回所有已发布的 OpenResty 配置版本摘要,需要管理员权限
|
description: 返回所有已发布的 OpenResty 配置版本摘要,按创建时间倒序排列,需要管理员权限
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
aggregateTrafficBreakdown,
|
aggregateTrafficBreakdown,
|
||||||
formatBytes,
|
formatBytes,
|
||||||
formatBytesPerSecond,
|
formatBytesPerSecond,
|
||||||
|
formatMetricCount,
|
||||||
formatPercent,
|
formatPercent,
|
||||||
formatRelativeTime,
|
formatRelativeTime,
|
||||||
formatUptime,
|
formatUptime,
|
||||||
@@ -455,11 +456,7 @@ export function NodeObservability({
|
|||||||
/>
|
/>
|
||||||
<MetricBar
|
<MetricBar
|
||||||
label="连接数"
|
label="连接数"
|
||||||
value={
|
value={formatMetricCount(latestMetric.openresty_connections)}
|
||||||
latestMetric.openresty_connections
|
|
||||||
? String(latestMetric.openresty_connections)
|
|
||||||
: '—'
|
|
||||||
}
|
|
||||||
progress={null}
|
progress={null}
|
||||||
hint={connectionHint}
|
hint={connectionHint}
|
||||||
/>
|
/>
|
||||||
@@ -521,11 +518,11 @@ export function NodeObservability({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SummaryStat
|
<SummaryStat
|
||||||
label="当前窗口请求"
|
label="请求/分钟"
|
||||||
value={trafficSummary ? trafficSummary.request_count.toLocaleString('zh-CN') : '—'}
|
value={formatMetricCount(trafficSummary?.request_count)}
|
||||||
hint={
|
hint={
|
||||||
trafficSummary
|
trafficSummary
|
||||||
? `QPS ${trafficSummary.estimated_qps.toFixed(1)} · 错误率 ${trafficSummary.error_rate_percent.toFixed(1)}%`
|
? `近 60 秒 · 错误率 ${trafficSummary.error_rate_percent.toFixed(1)}%`
|
||||||
: '当前没有可展示的请求窗口摘要'
|
: '当前没有可展示的请求窗口摘要'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -594,11 +591,7 @@ export function NodeObservability({
|
|||||||
/>
|
/>
|
||||||
<MetricBar
|
<MetricBar
|
||||||
label="连接数"
|
label="连接数"
|
||||||
value={
|
value={formatMetricCount(latestMetric.openresty_connections)}
|
||||||
latestMetric.openresty_connections
|
|
||||||
? String(latestMetric.openresty_connections)
|
|
||||||
: '—'
|
|
||||||
}
|
|
||||||
progress={null}
|
progress={null}
|
||||||
hint={connectionHint}
|
hint={connectionHint}
|
||||||
/>
|
/>
|
||||||
@@ -661,25 +654,23 @@ export function NodeObservability({
|
|||||||
<div className="grid gap-3 sm:grid-cols-2">
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
<div className="rounded-lg border px-3 py-3">
|
<div className="rounded-lg border px-3 py-3">
|
||||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||||
最近窗口请求
|
请求/分钟
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-3 text-2xl font-semibold">
|
<p className="mt-3 text-2xl font-semibold">
|
||||||
{trafficSummary
|
{formatMetricCount(trafficSummary?.request_count)}
|
||||||
? trafficSummary.request_count.toLocaleString('zh-CN')
|
|
||||||
: '—'}
|
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
{trafficSummary
|
{trafficSummary
|
||||||
? `QPS ${trafficSummary.estimated_qps.toFixed(1)} · UV ${trafficSummary.unique_visitor_count}`
|
? `近 60 秒 · UV ${formatMetricCount(trafficSummary.unique_visitor_count)}`
|
||||||
: '暂无窗口流量摘要'}
|
: '暂无窗口流量摘要'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border px-3 py-3">
|
<div className="rounded-lg border px-3 py-3">
|
||||||
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||||
最近窗口错误
|
近 60 秒错误
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-3 text-2xl font-semibold">
|
<p className="mt-3 text-2xl font-semibold">
|
||||||
{trafficSummary ? trafficSummary.error_count.toLocaleString('zh-CN') : '—'}
|
{formatMetricCount(trafficSummary?.error_count)}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
{trafficSummary
|
{trafficSummary
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import type {
|
import type {ApplyResult, NodeItem, NodeStatus, NodeTrafficReport, OpenrestyStatus,} from '@/lib/services/openflare';
|
||||||
ApplyResult,
|
|
||||||
NodeItem,
|
|
||||||
NodeStatus,
|
|
||||||
NodeTrafficReport,
|
|
||||||
OpenrestyStatus,
|
|
||||||
} from '@/lib/services/openflare';
|
|
||||||
|
|
||||||
export const WS_CONNECTED_LAST_SEEN = '__OPENFLARE_WS_CONNECTED__';
|
export const WS_CONNECTED_LAST_SEEN = '__OPENFLARE_WS_CONNECTED__';
|
||||||
export const FLARED_WS_CONNECTED_LAST_SEEN = '__OPENFLARE_FLARED_WS_CONNECTED__';
|
export const FLARED_WS_CONNECTED_LAST_SEEN = '__OPENFLARE_FLARED_WS_CONNECTED__';
|
||||||
@@ -176,6 +170,13 @@ export function formatPercent(value?: number | null) {
|
|||||||
return `${value.toFixed(1)}%`;
|
return `${value.toFixed(1)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatMetricCount(value?: number | null) {
|
||||||
|
if (value === undefined || value === null || !Number.isFinite(value)) {
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
return value.toLocaleString('zh-CN');
|
||||||
|
}
|
||||||
|
|
||||||
export function formatBytesPerSecond(
|
export function formatBytesPerSecond(
|
||||||
value?: number | null,
|
value?: number | null,
|
||||||
windowSeconds = 1,
|
windowSeconds = 1,
|
||||||
|
|||||||
@@ -35,6 +35,27 @@ type TrafficDistributions struct {
|
|||||||
SourceCountries []DistributionItem `json:"source_countries"`
|
SourceCountries []DistributionItem `json:"source_countries"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metricSnapshotOpenrestyMatchWindow = 2 * time.Minute
|
||||||
|
|
||||||
|
// NodeMetricSnapshotView is a metric snapshot enriched with OpenResty observations.
|
||||||
|
type NodeMetricSnapshotView struct {
|
||||||
|
ID uint `json:"id,omitempty"`
|
||||||
|
NodeID string `json:"node_id,omitempty"`
|
||||||
|
CapturedAt time.Time `json:"captured_at"`
|
||||||
|
CPUUsagePercent float64 `json:"cpu_usage_percent"`
|
||||||
|
MemoryUsedBytes int64 `json:"memory_used_bytes"`
|
||||||
|
MemoryTotalBytes int64 `json:"memory_total_bytes"`
|
||||||
|
StorageUsedBytes int64 `json:"storage_used_bytes"`
|
||||||
|
StorageTotalBytes int64 `json:"storage_total_bytes"`
|
||||||
|
DiskReadBytes int64 `json:"disk_read_bytes"`
|
||||||
|
DiskWriteBytes int64 `json:"disk_write_bytes"`
|
||||||
|
NetworkRxBytes int64 `json:"network_rx_bytes"`
|
||||||
|
NetworkTxBytes int64 `json:"network_tx_bytes"`
|
||||||
|
OpenrestyRxBytes int64 `json:"openresty_rx_bytes"`
|
||||||
|
OpenrestyTxBytes int64 `json:"openresty_tx_bytes"`
|
||||||
|
OpenrestyConnections int64 `json:"openresty_connections"`
|
||||||
|
}
|
||||||
|
|
||||||
// TrafficWindowSummary summarizes a traffic reporting window.
|
// TrafficWindowSummary summarizes a traffic reporting window.
|
||||||
type TrafficWindowSummary struct {
|
type TrafficWindowSummary struct {
|
||||||
WindowStartedAt time.Time `json:"window_started_at"`
|
WindowStartedAt time.Time `json:"window_started_at"`
|
||||||
@@ -112,9 +133,9 @@ type diskCounterState struct {
|
|||||||
seen bool
|
seen bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTrafficWindowSummary(report *model.OpenFlareRequestReport) TrafficWindowSummary {
|
func buildTrafficWindowSummary(report *model.OpenFlareRequestReport) *TrafficWindowSummary {
|
||||||
if report == nil {
|
if report == nil {
|
||||||
return TrafficWindowSummary{}
|
return nil
|
||||||
}
|
}
|
||||||
summary := TrafficWindowSummary{
|
summary := TrafficWindowSummary{
|
||||||
WindowStartedAt: report.WindowStartedAt,
|
WindowStartedAt: report.WindowStartedAt,
|
||||||
@@ -129,7 +150,44 @@ func buildTrafficWindowSummary(report *model.OpenFlareRequestReport) TrafficWind
|
|||||||
if report.RequestCount > 0 {
|
if report.RequestCount > 0 {
|
||||||
summary.ErrorRatePercent = (float64(report.ErrorCount) / float64(report.RequestCount)) * 100
|
summary.ErrorRatePercent = (float64(report.ErrorCount) / float64(report.RequestCount)) * 100
|
||||||
}
|
}
|
||||||
return summary
|
return &summary
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildMetricSnapshotViews merges metric snapshots with OpenResty observations for API responses.
|
||||||
|
func BuildMetricSnapshotViews(
|
||||||
|
snapshots []*model.OpenFlareMetricSnapshot,
|
||||||
|
openrestyObs []*model.OpenFlareNodeObservationOpenresty,
|
||||||
|
) []*NodeMetricSnapshotView {
|
||||||
|
if len(snapshots) == 0 {
|
||||||
|
return []*NodeMetricSnapshotView{}
|
||||||
|
}
|
||||||
|
views := make([]*NodeMetricSnapshotView, 0, len(snapshots))
|
||||||
|
for _, snapshot := range snapshots {
|
||||||
|
if snapshot == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
view := &NodeMetricSnapshotView{
|
||||||
|
ID: snapshot.ID,
|
||||||
|
NodeID: snapshot.NodeID,
|
||||||
|
CapturedAt: snapshot.CapturedAt,
|
||||||
|
CPUUsagePercent: snapshot.CPUUsagePercent,
|
||||||
|
MemoryUsedBytes: snapshot.MemoryUsedBytes,
|
||||||
|
MemoryTotalBytes: snapshot.MemoryTotalBytes,
|
||||||
|
StorageUsedBytes: snapshot.StorageUsedBytes,
|
||||||
|
StorageTotalBytes: snapshot.StorageTotalBytes,
|
||||||
|
DiskReadBytes: snapshot.DiskReadBytes,
|
||||||
|
DiskWriteBytes: snapshot.DiskWriteBytes,
|
||||||
|
NetworkRxBytes: snapshot.NetworkRxBytes,
|
||||||
|
NetworkTxBytes: snapshot.NetworkTxBytes,
|
||||||
|
}
|
||||||
|
if matched := matchOpenrestyObservation(snapshot.CapturedAt, openrestyObs); matched != nil {
|
||||||
|
view.OpenrestyRxBytes = matched.OpenrestyRxBytes
|
||||||
|
view.OpenrestyTxBytes = matched.OpenrestyTxBytes
|
||||||
|
view.OpenrestyConnections = matched.OpenrestyConnections
|
||||||
|
}
|
||||||
|
views = append(views, view)
|
||||||
|
}
|
||||||
|
return views
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildTrafficDistributions aggregates traffic distribution charts.
|
// BuildTrafficDistributions aggregates traffic distribution charts.
|
||||||
@@ -352,21 +410,54 @@ func BuildDiskIOTrendPoints(now time.Time, snapshots []*model.OpenFlareMetricSna
|
|||||||
}
|
}
|
||||||
|
|
||||||
func latestMetricSnapshot(snapshots []*model.OpenFlareMetricSnapshot) *model.OpenFlareMetricSnapshot {
|
func latestMetricSnapshot(snapshots []*model.OpenFlareMetricSnapshot) *model.OpenFlareMetricSnapshot {
|
||||||
|
var latest *model.OpenFlareMetricSnapshot
|
||||||
for _, snapshot := range snapshots {
|
for _, snapshot := range snapshots {
|
||||||
if snapshot != nil {
|
if snapshot == nil {
|
||||||
return snapshot
|
continue
|
||||||
|
}
|
||||||
|
if latest == nil || snapshot.CapturedAt.After(latest.CapturedAt) {
|
||||||
|
latest = snapshot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return latest
|
||||||
}
|
}
|
||||||
|
|
||||||
func latestTrafficReport(reports []*model.OpenFlareRequestReport) *model.OpenFlareRequestReport {
|
func latestTrafficReport(reports []*model.OpenFlareRequestReport) *model.OpenFlareRequestReport {
|
||||||
|
var latest *model.OpenFlareRequestReport
|
||||||
for _, report := range reports {
|
for _, report := range reports {
|
||||||
if report != nil {
|
if report == nil {
|
||||||
return report
|
continue
|
||||||
|
}
|
||||||
|
if latest == nil || report.WindowEndedAt.After(latest.WindowEndedAt) {
|
||||||
|
latest = report
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return latest
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchOpenrestyObservation(
|
||||||
|
capturedAt time.Time,
|
||||||
|
observations []*model.OpenFlareNodeObservationOpenresty,
|
||||||
|
) *model.OpenFlareNodeObservationOpenresty {
|
||||||
|
var matched *model.OpenFlareNodeObservationOpenresty
|
||||||
|
var bestDelta time.Duration = metricSnapshotOpenrestyMatchWindow + time.Second
|
||||||
|
for _, observation := range observations {
|
||||||
|
if observation == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
delta := capturedAt.Sub(observation.CapturedAt)
|
||||||
|
if delta < 0 {
|
||||||
|
delta = -delta
|
||||||
|
}
|
||||||
|
if delta > metricSnapshotOpenrestyMatchWindow {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if matched == nil || delta < bestDelta {
|
||||||
|
matched = observation
|
||||||
|
bestDelta = delta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matched
|
||||||
}
|
}
|
||||||
|
|
||||||
// LatestMetricSnapshotsByNode returns the latest snapshot per node.
|
// LatestMetricSnapshotsByNode returns the latest snapshot per node.
|
||||||
|
|||||||
@@ -51,4 +51,67 @@ func TestBuildTrafficTrendPointsBucketsByHour(t *testing.T) {
|
|||||||
if currentHour.ErrorCount != 0 {
|
if currentHour.ErrorCount != 0 {
|
||||||
t.Fatalf("current hour error_count = %d, want 0", currentHour.ErrorCount)
|
t.Fatalf("current hour error_count = %d, want 0", currentHour.ErrorCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildMetricSnapshotViewsMergesOpenrestyObservation(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
capturedAt := time.Date(2026, 6, 19, 12, 0, 0, 0, time.UTC)
|
||||||
|
snapshots := []*model.OpenFlareMetricSnapshot{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
NodeID: "node-a",
|
||||||
|
CapturedAt: capturedAt,
|
||||||
|
CPUUsagePercent: 12.5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
openrestyObs := []*model.OpenFlareNodeObservationOpenresty{
|
||||||
|
{
|
||||||
|
NodeID: "node-a",
|
||||||
|
CapturedAt: capturedAt.Add(5 * time.Second),
|
||||||
|
OpenrestyRxBytes: 4096,
|
||||||
|
OpenrestyTxBytes: 8192,
|
||||||
|
OpenrestyConnections: 7,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
views := BuildMetricSnapshotViews(snapshots, openrestyObs)
|
||||||
|
if len(views) != 1 {
|
||||||
|
t.Fatalf("BuildMetricSnapshotViews() len = %d, want 1", len(views))
|
||||||
|
}
|
||||||
|
if views[0].OpenrestyRxBytes != 4096 {
|
||||||
|
t.Fatalf("OpenrestyRxBytes = %d, want 4096", views[0].OpenrestyRxBytes)
|
||||||
|
}
|
||||||
|
if views[0].OpenrestyTxBytes != 8192 {
|
||||||
|
t.Fatalf("OpenrestyTxBytes = %d, want 8192", views[0].OpenrestyTxBytes)
|
||||||
|
}
|
||||||
|
if views[0].OpenrestyConnections != 7 {
|
||||||
|
t.Fatalf("OpenrestyConnections = %d, want 7", views[0].OpenrestyConnections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLatestTrafficReportUsesLatestWindowEndedAt(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
older := &model.OpenFlareRequestReport{
|
||||||
|
WindowEndedAt: time.Date(2026, 6, 19, 10, 0, 0, 0, time.UTC),
|
||||||
|
RequestCount: 3,
|
||||||
|
}
|
||||||
|
newer := &model.OpenFlareRequestReport{
|
||||||
|
WindowEndedAt: time.Date(2026, 6, 19, 11, 0, 0, 0, time.UTC),
|
||||||
|
RequestCount: 9,
|
||||||
|
}
|
||||||
|
|
||||||
|
latest := latestTrafficReport([]*model.OpenFlareRequestReport{older, newer})
|
||||||
|
if latest == nil || latest.RequestCount != 9 {
|
||||||
|
t.Fatalf("latestTrafficReport() = %#v, want newer report with request_count 9", latest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildTrafficWindowSummaryNilWithoutReport(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if summary := buildTrafficWindowSummary(nil); summary != nil {
|
||||||
|
t.Fatalf("buildTrafficWindowSummary(nil) = %#v, want nil", summary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ type NodeQuery struct {
|
|||||||
|
|
||||||
// NodeAnalytics groups node observability analytics.
|
// NodeAnalytics groups node observability analytics.
|
||||||
type NodeAnalytics struct {
|
type NodeAnalytics struct {
|
||||||
Traffic TrafficWindowSummary `json:"traffic"`
|
Traffic *TrafficWindowSummary `json:"traffic"`
|
||||||
Distributions TrafficDistributions `json:"distributions"`
|
Distributions TrafficDistributions `json:"distributions"`
|
||||||
Health HealthSummary `json:"health"`
|
Health HealthSummary `json:"health"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeTrends groups node observability trend series.
|
// NodeTrends groups node observability trend series.
|
||||||
@@ -66,7 +66,7 @@ type RelayProxyStat struct {
|
|||||||
type NodeView struct {
|
type NodeView struct {
|
||||||
NodeID string `json:"node_id"`
|
NodeID string `json:"node_id"`
|
||||||
Profile *model.OpenFlareNodeSystemProfile `json:"profile"`
|
Profile *model.OpenFlareNodeSystemProfile `json:"profile"`
|
||||||
MetricSnapshots []*model.OpenFlareMetricSnapshot `json:"metric_snapshots"`
|
MetricSnapshots []*NodeMetricSnapshotView `json:"metric_snapshots"`
|
||||||
TrafficReports []*model.OpenFlareRequestReport `json:"traffic_reports"`
|
TrafficReports []*model.OpenFlareRequestReport `json:"traffic_reports"`
|
||||||
HealthEvents []*model.OpenFlareHealthEvent `json:"health_events"`
|
HealthEvents []*model.OpenFlareHealthEvent `json:"health_events"`
|
||||||
Analytics NodeAnalytics `json:"analytics"`
|
Analytics NodeAnalytics `json:"analytics"`
|
||||||
@@ -103,6 +103,10 @@ func GetNodeObservability(ctx context.Context, id uint, query NodeQuery) (*NodeV
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
openrestyObs, err := model.ListOpenFlareNodeObservationOpenresty(ctx, node.NodeID, since, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
reports, err := model.ListOpenFlareRequestReportsSince(ctx, node.NodeID, since, limit)
|
reports, err := model.ListOpenFlareRequestReportsSince(ctx, node.NodeID, since, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -131,7 +135,7 @@ func GetNodeObservability(ctx context.Context, id uint, query NodeQuery) (*NodeV
|
|||||||
view := &NodeView{
|
view := &NodeView{
|
||||||
NodeID: node.NodeID,
|
NodeID: node.NodeID,
|
||||||
Profile: profile,
|
Profile: profile,
|
||||||
MetricSnapshots: snapshots,
|
MetricSnapshots: BuildMetricSnapshotViews(snapshots, openrestyObs),
|
||||||
TrafficReports: reports,
|
TrafficReports: reports,
|
||||||
HealthEvents: events,
|
HealthEvents: events,
|
||||||
Analytics: NodeAnalytics{
|
Analytics: NodeAnalytics{
|
||||||
|
|||||||
Reference in New Issue
Block a user