第一章:Go小型项目可观测性全景概览
可观测性不是监控的升级版,而是从“系统是否在运行”转向“系统为何如此运行”的范式转变。对Go小型项目而言,它由三大支柱构成:日志(Log)、指标(Metric)和链路追踪(Trace),三者协同还原真实运行状态。
日志是系统的行为快照
Go标准库log适合调试,但生产环境推荐结构化日志库如zerolog或zap。例如使用zerolog输出JSON日志:
import "github.com/rs/zerolog/log"
func main() {
log.Info().Str("service", "auth-api").Int("port", 8080).Msg("server started")
// 输出: {"level":"info","service":"auth-api","port":8080,"time":"2024-06-15T10:30:00Z","message":"server started"}
}
结构化日志便于ELK或Loki等工具解析与检索。
指标揭示系统健康趋势
Prometheus是Go生态事实标准。通过promhttp暴露指标端点,并用prometheus/client_golang注册自定义计数器:
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var reqCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{Name: "http_requests_total", Help: "Total HTTP requests"},
[]string{"method", "status"},
)
func init() {
prometheus.MustRegister(reqCounter)
}
func handler(w http.ResponseWriter, r *http.Request) {
reqCounter.WithLabelValues(r.Method, "200").Inc() // 记录成功请求
w.WriteHeader(http.StatusOK)
}
启动后访问/metrics即可获取文本格式指标,供Prometheus定时抓取。
链路追踪定位延迟瓶颈
借助OpenTelemetry SDK实现跨HTTP/gRPC调用的上下文透传。最小集成示例:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/trace"
)
func setupTracer() {
exporter, _ := otlptracehttp.New(context.Background())
tp := trace.NewTracerProvider(trace.WithBatcher(exporter))
otel.SetTracerProvider(tp)
}
配合Jaeger或Tempo后端,可可视化单次请求在各服务间的耗时分布。
| 组件 | 推荐工具 | 核心价值 |
|---|---|---|
| 日志 | zerolog + Loki | 快速定位错误上下文 |
| 指标 | Prometheus + Grafana | 发现资源使用异常与趋势拐点 |
| 追踪 | OpenTelemetry + Tempo | 定位高延迟Span与服务依赖热点 |
第二章:Prometheus指标埋点实战
2.1 Go应用指标类型选型与OpenTelemetry兼容性分析
Go 应用中指标采集需兼顾语义表达力与 OTel 规范对齐。核心指标类型应优先选用 Counter、Gauge 和 Histogram —— 它们在 OpenTelemetry SDK 中有原生映射,且符合 Prometheus 数据模型语义。
常见指标类型语义对照
| OpenTelemetry 类型 | Prometheus 等效 | 典型用途 | OTel Exporter 兼容性 |
|---|---|---|---|
Counter |
Counter | 请求总量、错误累计 | ✅ 全量支持 |
UpDownCounter |
Gauge | 并发 goroutine 数 | ✅(映射为 Gauge) |
Histogram |
Histogram | HTTP 延迟分布(P50/P99) | ✅(含 exemplars) |
Go SDK 实例:Histogram 与 OTel 语义对齐
// 创建符合 OTel 语义的直方图(自动启用 exemplar 支持)
hist := meter.NewHistogram("http.request.duration",
otlpmetric.WithDescription("HTTP request duration in seconds"),
otlpmetric.WithUnit("s"),
)
hist.Record(ctx, durationSeconds,
// OTel 标签(即 Prometheus label)
metric.WithAttributes(attribute.String("method", "GET"), attribute.String("status", "200")),
)
该调用生成符合 OTLP 协议的 HistogramDataPoint,其 explicit_bounds 与 exemplars 字段被 OpenTelemetry Collector 正确解析,无需额外适配层。WithUnit 和 WithDescription 直接映射至 OTel 的 unit 与 description 元数据字段,保障跨系统可观测性一致性。
2.2 使用promhttp和promauto实现HTTP服务端点自动暴露
promhttp 提供标准 /metrics 端点,而 promauto 进一步简化注册流程——无需手动调用 prometheus.MustRegister()。
自动注册机制
promauto.NewRegistry() 创建可自动注册指标的 registry,配合 promhttp.HandlerFor() 直接暴露:
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
reg := promauto.NewRegistry()
counter := promauto.With(reg).NewCounter(prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
})
http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
http.ListenAndServe(":8080", nil)
}
promauto.With(reg)绑定 registry,后续创建即自动注册;promhttp.HandlerFor()支持自定义编码格式与错误处理策略;HandlerOpts{}可配置EnableOpenMetrics,DisableCompression等行为。
核心优势对比
| 特性 | 手动注册 | promauto |
|---|---|---|
| 注册时机 | 显式调用 MustRegister() |
创建即注册 |
| 错误处理 | panic on duplicate | 自动命名去重(如加 _1) |
graph TD
A[定义指标] --> B{promauto.With\\(reg\\).NewCounter}
B --> C[自动注册到registry]
C --> D[promhttp.HandlerFor\\(reg\\)]
D --> E[HTTP响应/Metrics文本]
2.3 自定义业务指标(请求延迟、错误率、并发数)埋点编码实践
基础埋点封装:统一观测入口
使用拦截器/过滤器统一封装指标采集逻辑,避免散落式埋点:
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object traceMetrics(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
String path = ((MethodSignature) joinPoint.getSignature()).getMethod().getDeclaringClass().getSimpleName();
try {
Object result = joinPoint.proceed();
metricsRegistry.timer("api.latency", "path", path).record(System.nanoTime() - start, TimeUnit.NANOSECONDS);
return result;
} catch (Exception e) {
metricsRegistry.counter("api.errors", "path", path, "error", e.getClass().getSimpleName()).increment();
throw e;
} finally {
// 并发数通过 Gauge 动态上报
metricsRegistry.gauge("api.concurrency", () -> activeRequests.get());
}
}
逻辑分析:
timer()记录纳秒级延迟并自动聚合 P90/P99;counter()按路径与错误类型双维度打点;gauge()实时反映活跃请求数,activeRequests为AtomicInteger类型计数器。
关键参数说明
api.latency:命名空间前缀,便于 Prometheus 标签筛选"path"和"error":标签键,支持多维下钻分析TimeUnit.NANOSECONDS:确保精度兼容 Micrometer 默认单位
指标维度对照表
| 指标类型 | 核心标签 | 典型 PromQL 示例 |
|---|---|---|
| 请求延迟 | path, method |
histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[1h])) by (le, path)) |
| 错误率 | path, error, status |
rate(http_server_requests_errors_total[5m]) / rate(http_server_requests_total[5m]) |
| 并发数 | path, instance |
avg(http_server_requests_active{job="app"}) by (path) |
数据同步机制
graph TD
A[业务方法执行] --> B[拦截器捕获开始时间]
B --> C[异常捕获/结果返回]
C --> D[更新Timer/Counter/Gauge]
D --> E[MetricsRegistry 推送至Prometheus Pushgateway]
E --> F[Prometheus Server 定期拉取]
2.4 指标命名规范与标签(Label)设计原则及反模式规避
命名应体现维度与语义层级
指标名采用 namespace_subsystem_operation_suffix 格式,例如:
http_server_requests_total{method="POST",status="200",route="/api/users"}
✅ 含义清晰:http_server_requests_total 表明是 HTTP 服务端请求数总量;
✅ 标签分离关注点:method/status/route 是正交维度,支持任意组合下钻;
❌ 避免 http_post_200_users_total —— 将维度硬编码进名称,丧失聚合灵活性。
常见反模式对照表
| 反模式 | 问题 | 推荐替代 |
|---|---|---|
cpu_usage_percent_core0 |
核心编号嵌入名称,无法按 core 标签聚合 | node_cpu_seconds_total{mode="idle",cpu="0"} |
db_query_latency_ms |
单位隐含在名称中,违反 Prometheus 单位统一原则 | db_query_duration_seconds(单位始终为秒) |
标签设计黄金法则
- ✅ 必须可选、低基数(env="prod")
- ❌ 禁止使用高基数字段(如
user_id,request_id)——易导致内存爆炸与查询抖动 - ⚠️ 业务标识类标签(如
tenant_id)需严格准入评审
graph TD
A[原始日志] --> B{是否含稳定业务维度?}
B -->|是| C[提取为 label]
B -->|否| D[转为 metric 属性或丢弃]
C --> E[检查基数 & 变更频率]
E -->|低基数+低频变更| F[纳入 target labels]
E -->|高基数/高频变更| G[降级为 exemplar 或 trace 关联]
2.5 指标生命周期管理:注册、复用、清理与测试验证
指标不是一次定义终身可用的静态实体,而需纳入闭环生命周期治理。
注册:统一入口与元数据契约
通过 MetricRegistry 实现声明式注册,强制携带语义标签与计算口径:
registry.register(
name="order_conversion_rate", # 唯一标识符(命名空间+业务语义)
expr="count(completed_orders) / count(visited_landing_page)", # DSL表达式
tags={"domain": "ecommerce", "granularity": "daily"}, # 用于路由与权限控制
owner="team-ux@company.com" # 责任人,触发告警与变更审批
)
该注册动作同步写入元数据中心,为后续复用与血缘追踪提供唯一事实源。
复用与清理策略
- ✅ 复用:依赖
metric_id(非名称)跨报表/告警引用,避免硬编码 - ❌ 清理:满足「90天未被查询 + 无下游依赖」双条件才进入软删除队列
| 状态 | 触发条件 | 自动化动作 |
|---|---|---|
ACTIVE |
注册成功且至少1次查询 | 开放API访问 |
DEPRECATED |
所有下游已切换至新指标 | 拒绝新引用,保留历史数据 |
ARCHIVED |
超过180天无访问且无依赖 | 数据归档,释放计算资源 |
测试验证:保障指标语义一致性
graph TD
A[提交指标变更] --> B{语法与血缘校验}
B -->|通过| C[生成沙箱SQL]
B -->|失败| D[阻断合并]
C --> E[对比黄金数据集]
E -->|Δ<0.1%| F[自动发布]
E -->|Δ≥0.1%| G[人工介入]
第三章:Grafana看板构建与数据可视化
3.1 Prometheus数据源配置与查询表达式进阶(rate、histogram_quantile等)
数据源配置要点
在 Grafana 中添加 Prometheus 数据源时,需启用 Direct 访问模式,并勾选 Skip TLS verification(仅限测试环境)。关键参数:
HTTP URL:http://prometheus:9090Scrape interval: 建议与 Prometheus 全局scrape_interval一致(如15s)
rate() 的时间窗口陷阱
# ✅ 正确:rate 应覆盖至少 4 个采样点(推荐 2×scrape_interval)
rate(http_requests_total[5m])
# ❌ 错误:[1m] 在 15s 采集间隔下仅约 4 个点,易受瞬时抖动干扰
rate(http_requests_total[1m])
rate() 自动外推并处理计数器重置;其 [5m] 表示滑动窗口内拟合斜率,非简单差值除以5分钟。
histogram_quantile 实战解析
| 参数 | 含义 | 示例 |
|---|---|---|
φ (phi) |
分位数(0–1) | 0.95 表示 P95 |
le |
直方图标签上界 | le="0.2" 对应 ≤200ms 桶 |
sum by |
必须按 le 分组聚合 |
sum by(le)(...) |
# 计算 P90 响应延迟(单位:秒)
histogram_quantile(0.90, sum by(le)(rate(http_request_duration_seconds_bucket[5m])))
该表达式先用 rate 计算每桶每秒速率,再由 histogram_quantile 插值得到分位数——忽略 sum by(le) 将导致结果失真。
查询性能优化路径
- 避免在高基数指标上直接
rate() histogram_quantile前务必sum by(le)聚合- 使用
label_replace()预处理动态标签可提升缓存命中率
graph TD
A[原始直方图桶] --> B[rate<br>→ 每秒事件速率]
B --> C[sum by le<br>→ 汇总各桶速率]
C --> D[histogram_quantile<br>→ 插值计算分位数]
3.2 构建可复用的Go服务核心看板(QPS、P95延迟、内存/ goroutine趋势)
统一指标采集接口
定义标准化 MetricsCollector 接口,解耦业务逻辑与监控上报:
type MetricsCollector interface {
ObserveQPS(duration time.Duration)
ObserveLatency(latency time.Duration) // P95 计算基于滑动窗口直方图
ReportMemoryStats()
ReportGoroutineCount()
}
该接口封装了四类核心观测能力:
ObserveQPS基于time.Since()累计请求频次;ObserveLatency使用prometheus.HistogramVec支持分路径P95聚合;后两者调用runtime.ReadMemStats()与runtime.NumGoroutine()实时采样。
指标聚合策略对比
| 策略 | QPS精度 | P95实时性 | 内存开销 | 适用场景 |
|---|---|---|---|---|
| 全量采样+Prometheus | 高 | 中 | 低 | 生产环境主看板 |
| 滑动窗口本地聚合 | 中 | 高 | 中 | 边缘服务轻量监控 |
数据同步机制
使用带缓冲通道异步推送指标,避免阻塞业务goroutine:
func (c *DefaultCollector) StartSync(interval time.Second) {
ticker := time.NewTicker(interval)
go func() {
for range ticker.C {
c.syncToPrometheus() // 调用 prometheus.MustRegister()
}
}()
}
syncToPrometheus()将本地统计快照注册为prometheus.Gauge和Histogram类型,确保/metrics端点可被 Prometheus server 拉取。缓冲通道容量设为1024,防止高并发下丢弃关键指标。
3.3 看板版本化管理与JSON导出/导入自动化流程
版本化核心设计
看板状态以语义化版本号(vMAJOR.MINOR.PATCH)标识,每次字段变更、列增删或权限调整均触发版本递增,并自动生成带时间戳的快照。
JSON导出脚本(含校验)
#!/bin/bash
# 导出当前看板为带版本信息的JSON
BOARD_ID="prod-kanban-2024"
VERSION=$(jq -r '.version' config.json | sed 's/v//')
jq --arg ver "$VERSION" --arg id "$BOARD_ID" \
'{version: $ver, boardId: $id, timestamp: (now | strftime "%Y-%m-%dT%H:%M:%SZ"), data: .}' \
kanban-state.json > "kanban_v${VERSION}_${BOARD_ID}.json"
逻辑说明:
--arg注入动态变量;now | strftime生成ISO 8601时间戳;嵌套结构确保元数据与业务数据分离。参数VERSION来自配置文件,保障版本一致性。
自动化流程依赖
| 步骤 | 工具 | 触发条件 |
|---|---|---|
| 导出 | jq + curl |
Git commit 含 [kanban:export] |
| 校验 | jsonschema |
文件名匹配 kanban_v\d+\.\d+\.\d+_.*\.json |
| 导入 | Python CLI | kanban-cli import --force --version=1.2.0 |
graph TD
A[Git Push] --> B{Commit Message 匹配?}
B -->|Yes| C[执行 export.sh]
C --> D[生成带签名JSON]
D --> E[推送至 artifact repo]
第四章:Alertmanager告警全链路落地
4.1 告警规则编写:基于Prometheus Rule语法定义SLO违背条件
SLO违背的核心逻辑
SLO(Service Level Objective)违背本质是「错误率持续超出目标阈值」。Prometheus Rule通过expr计算黄金指标比率,结合for实现持续性判定。
基础告警示例
- alert: SLO_ErrorBudgetBurnRateHigh
expr: |
(sum(rate(http_request_duration_seconds_count{code=~"5.."}[1h]))
/ sum(rate(http_request_duration_seconds_count[1h]))) > 0.01
for: 15m
labels:
severity: critical
slo_target: "99%"
逻辑分析:该表达式计算过去1小时HTTP 5xx错误率;
for: 15m确保异常持续15分钟才触发,避免瞬时抖动误报;slo_target标签便于后续按SLO维度聚合告警。
关键参数对照表
| 参数 | 作用 | 推荐值 |
|---|---|---|
expr |
定义SLO违背的瞬时条件 | 使用rate()+sum()组合计算比率 |
for |
持续观察窗口 | ≥ SLO评估周期的1/4(如28d SLO建议≥1h) |
告警触发流程
graph TD
A[采集指标] --> B[Rule Engine执行expr]
B --> C{结果 > 阈值?}
C -->|是| D[启动for计时器]
C -->|否| E[重置计时器]
D --> F{持续满足?}
F -->|是| G[触发告警]
4.2 Alertmanager高可用部署与路由分组策略(service、severity、team标签路由)
Alertmanager 高可用依赖于集群模式与一致哈希分片,避免单点故障与告警重复。
数据同步机制
通过 --cluster.advertise-address 和 --cluster.peer 自动构建 Gossip 网络,状态(如抑制、静默)在节点间最终一致。
路由分组核心逻辑
基于标签动态分发告警,典型配置:
route:
group_by: [service, severity, team]
routes:
- match:
service: "payment"
severity: "critical"
receiver: "team-payments-pagerduty"
- match:
team: "frontend"
receiver: "team-frontend-slack"
逻辑分析:
group_by将相同service+severity+team组合的告警聚合为单一通知;match按标签层级匹配,优先级由路由嵌套深度决定。team标签实现责任归属,severity控制响应 SLA,service隔离业务域。
高可用拓扑示意
graph TD
A[Prometheus] --> B[AM-1]
A --> C[AM-2]
A --> D[AM-3]
B <--> C <--> D
B & C & D --> E[(PagerDuty/Slack)]
| 标签 | 用途 | 示例值 |
|---|---|---|
service |
服务粒度隔离 | auth, cart |
severity |
告警紧急程度分级 | info, critical |
team |
责任团队归属 | backend, infra |
4.3 邮件/企业微信/钉钉多通道告警集成与模板渲染实践
统一告警路由设计
采用策略模式解耦通道逻辑,通过 channel_type 动态分发至对应适配器:
# alert_router.py
def send_alert(alert: Alert, channel_type: str):
adapter = {
"email": EmailAdapter(),
"wxwork": WxWorkAdapter(),
"dingtalk": DingTalkAdapter()
}.get(channel_type)
return adapter.render_and_send(alert)
Alert 对象封装告警级别、服务名、异常堆栈等结构化字段;render_and_send() 先调用 Jinja2 模板引擎注入上下文,再执行通道专属协议(SMTP/HTTPS)。
模板渲染能力对比
| 通道 | 支持变量 | 消息格式 | 限流策略 |
|---|---|---|---|
| 邮件 | {{ .service }} |
HTML/Text | SMTP 连接池 |
| 企业微信 | {{ .severity }} |
Markdown | API 调用频控 |
| 钉钉 | {{ .trace_id }} |
ActionCard | Webhook 签名校验 |
多通道协同流程
graph TD
A[告警事件] --> B{路由决策}
B -->|email| C[渲染HTML模板]
B -->|wxwork| D[生成Markdown卡片]
B -->|dingtalk| E[构造ActionCard JSON]
C --> F[SMTP发送]
D --> G[企业微信API]
E --> H[钉钉Webhook]
4.4 告警抑制、静默与响应闭环:从触发到确认的可观测性工作流
告警洪流是可观测性落地的最大障碍之一。真正的成熟度不在于检测能力,而在于精准干预与闭环验证。
告警抑制策略示例(Prometheus Alertmanager)
# alertmanager.yml 片段:基于标签动态抑制
route:
group_by: ['alertname', 'cluster']
receiver: 'pagerduty'
# 抑制规则:当高优先级维护事件触发时,暂抑所有关联服务告警
inhibit_rules:
- source_match:
alertname: "MaintenanceScheduled"
severity: "critical"
target_match_re:
service: "^(api|auth|db)-.*"
equal: ['cluster', 'environment']
逻辑分析:inhibit_rules 在 Alertmanager 内部构建 DAG 关系图,仅当 source_match 告警处于 active 状态且满足 target_match_re 正则匹配时,才临时屏蔽目标告警。equal 字段确保抑制作用域严格限定于同集群/环境,避免跨域误抑。
静默生命周期管理
- 创建:通过 API 或 UI 提交带
startsAt/endsAt和匹配器(如{job="backend", env="prod"})的静默规则 - 持久化:静默状态存储于 etcd,支持 TTL 自动清理
- 审计:每次激活/过期均写入审计日志,关联操作人与变更原因
响应闭环关键指标
| 指标 | 目标值 | 说明 |
|---|---|---|
| MTTR(告警→确认) | 从 alert firing 到 SRE 手动标记 acknowledged 的中位时长 |
|
| 抑制命中率 | 65%–80% | 被成功抑制的告警数 / 总告警数,过高表明规则过度宽泛 |
graph TD
A[Alert Fired] --> B{是否匹配抑制规则?}
B -->|是| C[暂不通知]
B -->|否| D[路由至接收器]
D --> E[PagerDuty/SMS/Slack]
E --> F[工程师点击 Ack]
F --> G[状态更新为 acknowledged]
G --> H[自动触发诊断脚本 & 记录根因字段]
第五章:结语:轻量级可观测性演进路径与最佳实践
轻量级可观测性并非简单地“删减功能”,而是围绕业务价值密度重构数据采集、处理与反馈闭环。某电商SaaS服务商在Q3大促前将原有ELK+Prometheus+Jaeger三栈架构重构为基于OpenTelemetry Collector + Grafana Loki + Tempo的轻量组合,整体资源开销下降62%,告警平均响应时间从4.8分钟压缩至57秒。
演进不是线性替换,而是分阶段能力沉淀
该团队采用三阶段演进模型:
- 第一阶段(0→1):统一指标与日志格式,强制所有Go/Python服务注入OTLP exporter,禁用自定义埋点SDK;
- 第二阶段(1→N):基于Grafana Tempo构建Trace-to-Log关联链路,通过
traceID字段自动注入Loki日志流标签; - 第三阶段(N→∞):在CI流水线中嵌入可观测性健康检查——若新版本部署后
http.server.durationP95上升超15%或log_level=error突增3倍,自动回滚并触发根因分析任务。
工具选型必须匹配组织成熟度
下表对比了不同规模团队的典型技术组合:
| 团队规模 | 核心组件 | 数据保留策略 | 运维人力投入 |
|---|---|---|---|
| 5人初创 | Grafana Cloud(托管Loki+Tempo) | 日志7天,Trace30天 | |
| 50人中台 | 自建OTel Collector集群+VictoriaMetrics | 全量日志3天+采样Trace90天 | 1.2人日/周 |
| 200人平台 | 多租户OTel Gateway+ClickHouse日志仓库 | 按业务域分级保留(核心服务全量) | 3.5人日/周 |
警惕“轻量”陷阱:性能与可维护性的再平衡
某金融客户曾将采样率从100%降至1%,导致支付失败场景无法定位到具体SQL参数。后续通过动态采样策略修复:对/api/v1/pay等关键路径保持100%采样,非核心路径按错误率动态提升采样率(错误率>0.1%时升至20%)。其OTel配置片段如下:
processors:
probabilistic_sampler:
hash_seed: 42
sampling_percentage: 1.0
decision_weight: 0.9
attribute_rules:
- key: http.url
values: ["/api/v1/pay", "/api/v1/refund"]
enabled: true
文化与流程比工具更重要
该公司将可观测性SLI(如request_success_rate)写入每个微服务的README.md,并要求PR描述中必须包含本次变更对SLI的影响预估。运维团队每月发布《可观测性健康报告》,其中包含各服务Trace深度覆盖率(当前均值83%)、日志结构化率(92%)、指标缺失率(
成本控制需贯穿全生命周期
他们通过eBPF探针替代应用层SDK采集HTTP状态码与延迟,在Node.js服务中减少17% CPU占用;同时利用Loki的chunk_encoding=zstd与分区策略(按service_name和date二级分区),使日志存储成本降低44%。Mermaid流程图展示了其数据流向优化:
flowchart LR
A[Application] -->|OTLP/gRPC| B[OTel Collector]
B --> C{Routing Rule}
C -->|Critical Path| D[Loki Full Retention]
C -->|Non-critical| E[Loki Sampled Retention]
C -->|Error Trace| F[Tempo Storage]
D --> G[Grafana Dashboard]
E --> G
F --> G 