第一章:【东北首个Golang可观测性实践】:基于OpenTelemetry+Grafana+大连本地时序数据库的零成本监控方案
大连某金融科技团队在2024年初完成东北地区首个全链路Golang服务可观测性落地——不依赖SaaS云服务,不采购商业APM,完全复用本地政务云已部署的大连时序数据库(DTDB,基于VictoriaMetrics深度定制的国产化时序存储),实现日均1.2亿指标、80万Span、45万Log事件的实时采集与分析。
架构设计原则
- 零新增基础设施:复用现有DTDB集群(v1.92.0-dalian-edition),仅需开放
/api/v1/import/prometheus写入端点; - Go原生集成:所有微服务统一使用
go.opentelemetry.io/otel/sdk/metric+go.opentelemetry.io/otel/exporters/otlp/otlptrace; - 协议轻量化:Trace数据通过OTLP/gRPC直传,Metrics经OTLP/HTTP转为Prometheus格式后批量推送到DTDB(避免采样失真)。
OpenTelemetry SDK初始化示例
// 初始化TracerProvider(自动注入DTDB Collector地址)
tp := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.AlwaysSample()),
sdktrace.WithSpanProcessor(sdktrace.NewBatchSpanProcessor(
otlptracehttp.NewClient(otlptracehttp.WithEndpoint("dt-collector:4318")), // 本地K8s Service
)),
)
otel.SetTracerProvider(tp)
// Metrics导出器:将OTLP Metrics转换为Prometheus格式并推送至DTDB
exporter, _ := prometheus.New(prometheus.WithRegisterer(nil))
controller := metric.NewController(
metric.NewNoopMeterProvider(),
metric.WithExporter(exporter),
metric.WithCollectPeriod(15 * time.Second),
)
Grafana数据源配置关键项
| 字段 | 值 | 说明 |
|---|---|---|
| URL | https://dt-db-gateway.dalian.gov/api/v1 |
DTDB网关地址(含反向代理TLS终止) |
| Scrape Interval | 15s |
与Metrics采集周期严格对齐 |
| HTTP Method | POST |
必须启用,DTDB仅接受POST /api/v1/import/prometheus |
零成本实现路径
- 所有组件(OTel Collector、Grafana、DTDB)均运行于政务云已有K8s集群;
- DTDB已通过等保三级认证,无需额外安全加固投入;
- Grafana仪表盘模板全部开源(GitHub:
dl-observability/dalian-go-dashboard),含Goroutine阻塞率、HTTP延迟P99热力图、DB连接池饱和度预警等12个核心视图。
第二章:OpenTelemetry在大连Golang微服务中的深度集成
2.1 OpenTelemetry Go SDK核心原理与生命周期管理
OpenTelemetry Go SDK 的核心是 sdktrace.TracerProvider,它统一管理 Tracer 实例、采样策略、处理器(SpanProcessor)及资源(Resource)绑定,其生命周期严格遵循 Go 的初始化—运行—关闭三阶段。
资源与提供者初始化
import "go.opentelemetry.io/otel/sdk/resource"
res, _ := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("auth-service"),
),
)
resource.Merge 合并默认环境信息与自定义服务元数据;SchemaURL 确保语义约定版本一致性;ServiceNameKey 是 OpenTelemetry 标准标识符,用于后端服务发现。
生命周期关键方法
TracerProvider.Shutdown():阻塞式关闭所有处理器,确保未导出 Span 刷写完成TracerProvider.ForceFlush():非阻塞强制刷新,适用于超时敏感场景TracerProvider.RegisterSpanProcessor():动态注册处理器,支持热插拔日志/指标桥接器
Span 处理器协作流程
graph TD
A[StartSpan] --> B[SpanBuilder]
B --> C[SpanProcessor.OnStart]
C --> D[In-Memory Buffer]
D --> E[Exporter.Export]
E --> F[HTTP/gRPC 传输]
| 组件 | 线程安全 | 可重入 | 关闭依赖 |
|---|---|---|---|
| TracerProvider | ✅ | ❌ | 所有 Processor |
| BatchSpanProcessor | ✅ | ✅ | Exporter |
| SimpleSpanProcessor | ✅ | ✅ | Exporter(无缓冲) |
2.2 基于大连真实电商订单服务的Tracing埋点实战
在大连某头部生鲜电商平台的订单履约系统中,我们基于 OpenTelemetry SDK 对核心链路进行轻量级 Tracing 埋点。
埋点关键路径
- 订单创建(
/api/v1/order/submit) - 库存预占(
InventoryService.reserve()) - 支付回调监听(
PaymentWebhookHandler)
OpenTelemetry Java Agent 配置示例
// 在 Spring Boot 启动类中注入全局 tracer
@Bean
public Tracer tracer() {
return OpenTelemetrySdk.builder()
.setTracerProvider(TracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(
OtlpGrpcSpanExporter.builder()
.setEndpoint("http://jaeger-collector:4317") // 指向本地 Jaeger Collector
.build())
.build())
.build())
.build()
.getTracer("order-service", "1.5.0");
}
该配置启用 gRPC 协议上报 span 数据;order-service 为服务名,1.5.0 为语义化版本,用于后端按服务+版本维度聚合分析。
核心 Span 属性映射表
| 字段 | 来源 | 示例值 |
|---|---|---|
http.status_code |
Servlet Filter | 201 |
order.id |
请求体解析 | DL2024052110482293 |
span.kind |
自动标注 | SERVER / CLIENT |
graph TD
A[HTTP POST /order/submit] --> B[Validate & Parse]
B --> C[Start Span: order.submit]
C --> D[Call InventoryService]
D --> E[End Span with error tag if failed]
2.3 Metrics采集策略设计:从Goroutine到HTTP延迟的维度建模
为精准刻画服务性能瓶颈,需构建多粒度、正交可组合的指标维度模型。
维度建模原则
- 正交性:
service、endpoint、status_code、http_method等标签互不冗余 - 可观测性优先:高基数标签(如
user_id)默认禁用,仅按需开启 - 成本可控:
histogram_quantile()计算在 Prometheus 端完成,避免客户端聚合开销
Goroutine 与 HTTP 延迟协同采集示例
// 使用同一 Observer 实例复用标签逻辑,降低内存分配
httpDuration := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Latency distribution of HTTP requests",
Buckets: prometheus.ExponentialBuckets(0.001, 2, 12), // 1ms~2s
},
[]string{"service", "endpoint", "method", "status_code"},
)
该配置定义了 4 维标签组合的延迟直方图;ExponentialBuckets 覆盖微秒级 API 到秒级长尾请求,避免线性桶在高并发下内存膨胀。
| 维度 | 示例值 | 采集频率 | 说明 |
|---|---|---|---|
goroutines |
128 | 15s | 进程级并发数,反映阻塞风险 |
http_latency |
p99=0.42s | 请求级 | 关联 endpoint+status_code 定位异常路径 |
graph TD
A[HTTP Handler] --> B[Start Timer]
B --> C[业务逻辑执行]
C --> D[Observe Latency with Labels]
D --> E[Prometheus Exporter]
F[Goroutine Count] --> E
2.4 日志关联(Log-Trace-Metric三合一)在大连本地K8s集群中的落地验证
在大连IDC自建的Kubernetes v1.28集群中,通过OpenTelemetry Collector统一采集日志、链路与指标,并注入trace_id和span_id至Fluent Bit输出的JSON日志字段。
数据同步机制
使用OTel Collector的resource_detection与attributes处理器为所有信号注入集群标识:
processors:
attributes/add-cluster:
actions:
- key: "k8s.cluster.name"
value: "dl-prod-cluster"
action: insert
该配置确保日志、trace、metric共享统一资源上下文,为后端Loki+Tempo+Prometheus联合查询提供语义锚点。
关联验证结果
| 组件 | 关联字段示例 | 查询验证方式 |
|---|---|---|
| Loki日志 | trace_id="0xabc123..." |
{job="app"} | trace_id |
| Tempo trace | service.name="order-api" |
按日志中trace_id反查调用链 |
| Prometheus | trace_id作为label标签 |
rate(http_request_duration_seconds_count{trace_id=~".+"}[5m]) |
graph TD
A[应用Pod] -->|OTel SDK| B[OTel Collector]
B --> C[Loki 日志存储]
B --> D[Tempo 链路存储]
B --> E[Prometheus 指标存储]
C & D & E --> F[Granafa统一仪表盘]
2.5 资源受限场景下采样率动态调优与内存泄漏规避实践
在嵌入式设备或边缘网关等内存紧张环境中,固定高频采样易引发OOM与数据积压。需建立“反馈驱动”的自适应采样机制。
动态采样率决策逻辑
基于实时内存水位与CPU负载双阈值触发降频:
# 根据系统资源动态调整采样间隔(毫秒)
def calc_sampling_interval(mem_usage_pct: float, cpu_load: float) -> int:
if mem_usage_pct > 85 or cpu_load > 0.9:
return 5000 # 降为2Hz(严重过载)
elif mem_usage_pct > 70 or cpu_load > 0.7:
return 2000 # 降为5Hz(中度压力)
else:
return 500 # 恢复20Hz(正常)
逻辑说明:
mem_usage_pct取自psutil.virtual_memory().percent;cpu_load为1分钟均值;返回值直接映射至time.sleep()周期,避免浮点精度误差导致的定时漂移。
内存泄漏防护要点
- ✅ 使用弱引用缓存传感器元数据
- ✅ 所有
threading.Timer对象显式调用.cancel() - ❌ 禁止在闭包中持有
self引用
| 风险点 | 安全替代方案 |
|---|---|
| 全局字典缓存实例 | weakref.WeakValueDictionary |
| 未清理的观察者回调 | 注册时返回token,退出前unregister(token) |
资源监控闭环流程
graph TD
A[采集线程] --> B{内存/CPU采样}
B --> C[评估水位]
C -->|超阈值| D[下发新采样间隔]
C -->|正常| E[维持当前频率]
D --> F[重置定时器+清空待发缓冲区]
第三章:Grafana可视化体系的大连定制化构建
3.1 基于大连政务云网络拓扑的Dashboard分层架构设计
结合大连政务云“一云多域、跨AZ高可用”的网络特征,Dashboard采用四层解耦架构:接入层(HTTPS+国密SM2双向认证)、网关层(Kong集群按委办局路由分流)、服务层(微服务按安全等级隔离部署)、数据层(分库分表+动态数据源路由)。
核心路由策略
# kong.yaml 片段:基于X-Dept-Code头实现委办局流量隔离
routes:
- name: dashboard-health-route
paths: ["/api/health"]
methods: ["GET"]
# 全局可访问
- name: dashboard-data-route
paths: ["/api/v1/metrics"]
headers:
X-Dept-Code: "dlswj|dlzjj|dlrsj" # 白名单委办局编码
逻辑分析:X-Dept-Code作为可信上下文透传标识,由前置WAF统一注入;Kong通过正则匹配实现租户级路由隔离,避免服务间越权调用。dlswj等编码与政务云IAM系统实时同步。
数据源路由映射表
| 委办局编码 | 主库实例 | 读库实例组 | 加密策略 |
|---|---|---|---|
| dlswj | pg-dlswj-primary | pg-dlswj-ro-01 | 国密SM4 |
| dlzjj | pg-dlzjj-primary | pg-dlzjj-ro-01 | AES-256-GCM |
架构演进流程
graph TD
A[用户请求] --> B{Kong网关}
B -->|X-Dept-Code=dlswj| C[SWJ专属服务集群]
B -->|X-Dept-Code=dlzjj| D[ZJJ专属服务集群]
C --> E[动态路由至dlswj数据源]
D --> F[动态路由至dlzjj数据源]
3.2 Golang运行时指标(GC Pause、Heap Allocs、Goroutines)的语义化看板开发
语义化看板需将原始指标映射为业务可理解的状态。核心指标通过 runtime.ReadMemStats 和 debug.ReadGCStats 获取,再经标准化标签注入 Prometheus 客户端。
数据同步机制
采用带缓冲的 ticker 每 5 秒采集一次,避免 GC 阻塞影响采样频率:
ticker := time.NewTicker(5 * time.Second)
for range ticker.C {
var m runtime.MemStats
runtime.ReadMemStats(&m)
memGauge.WithLabelValues("heap_alloc").Set(float64(m.HeapAlloc))
// Goroutine 数量直接读取,轻量且实时
gorosGauge.Set(float64(runtime.NumGoroutine()))
}
HeapAlloc表示当前已分配但未释放的堆内存字节数(非峰值),是内存压力关键信号;NumGoroutine()返回瞬时活跃协程数,突增常预示泄漏或调度异常。
指标语义分层
| 指标名 | 语义层级 | 告警建议阈值 |
|---|---|---|
gc_pause_p99 |
稳定性 | > 10ms(低延迟服务) |
heap_alloc |
容量 | > 80% of heap limit |
goroutines |
并发健康 | > 5000(无明确上限) |
可视化逻辑流
graph TD
A[Runtime API] --> B{采集器}
B --> C[语义标注:env=prod, svc=auth]
C --> D[Prometheus Pushgateway]
D --> E[Grafana 多维下钻看板]
3.3 多租户场景下大连企业级告警规则模板库建设
为支撑本地金融、制造类客户差异化SLO要求,模板库采用“租户基线+行业扩展”双层建模机制。
核心设计原则
- 租户隔离:通过
tenant_id+template_scope(global/industry/custom)联合索引保障查询性能 - 动态继承:子租户可覆盖父租户规则,但仅限阈值与通知渠道字段
规则元数据结构(简化版)
# templates/dl_bank_payment_delay.yaml
metadata:
id: "DL-BANK-PAY-001"
name: "核心支付延迟超阈值"
tenant_id: "dl-bank-prod" # 租户唯一标识
scope: "industry" # 作用域:影响所有银行类租户
version: "2.3"
spec:
promql: |
rate(payment_duration_seconds_sum{job="payment-gateway"}[5m])
/ rate(payment_duration_seconds_count{job="payment-gateway"}[5m])
> {{ .threshold }} # 支持Jinja2模板变量注入
severity: "critical"
thresholds:
default: 1.8
dl-bank-prod: 1.2 # 租户级覆盖
逻辑分析:该YAML通过
tenant_id与scope实现策略分发控制;{{ .threshold }}在渲染时动态注入,避免硬编码;dl-bank-prod专属阈值体现本地化SLA适配能力。
模板加载流程
graph TD
A[租户请求告警配置] --> B{查缓存}
B -- 命中 --> C[返回渲染后RuleSet]
B -- 未命中 --> D[读取YAML模板]
D --> E[注入租户上下文]
E --> F[生成Prometheus Rule文件]
F --> C
| 字段 | 类型 | 说明 |
|---|---|---|
scope |
string | global(全平台)、industry(大连金融/重工行业)、custom(租户私有) |
version |
semver | 支持灰度发布与回滚 |
thresholds |
map[string]float64 | 键为租户ID,支持细粒度覆盖 |
第四章:大连本地时序数据库(DalianTSDB)与可观测栈协同优化
4.1 DalianTSDB存储引擎特性解析及其与OTLP协议兼容性适配
DalianTSDB采用分层时间分区+列式压缩存储架构,原生支持毫秒级时间戳对齐与标签索引加速。
数据同步机制
OTLP/HTTP 接入层通过 otelcol 转发时,需启用 dalian_tsdb_exporter 插件:
exporters:
dalian_tsdb:
endpoint: "http://tsdb-gw:8086/write"
# 必须启用 OTLP v0.32+ 兼容模式
use_otlp_semantic_convention: true
该配置触发引擎自动将 resource_attributes 映射为 _meta 标签族,并将 instrumentation_scope 编码为 _scope 索引字段。
存储格式映射对比
| OTLP 字段 | DalianTSDB 内部列 | 类型 | 说明 |
|---|---|---|---|
time_unix_nano |
t |
int64 | 自动截断纳秒为毫秒精度 |
attributes["job"] |
job |
string | 直接提升为一级维度标签 |
exemplars |
_exemplar |
jsonb | 支持采样上下文嵌套存储 |
协议适配流程
graph TD
A[OTLP Trace/Logs/Metrics] --> B{otelcol 接收}
B --> C[Normalize semantic conventions]
C --> D[DalianTSDB Exporter]
D --> E[Schema-aware write path]
E --> F[Time-partitioned WAL + LSM merge]
4.2 面向Golang pprof profile数据的时序化存储与反向索引构建
Golang pprof 生成的二进制 profile(如 cpu.pprof, heap.pprof)需结构化解析后持久化,以支撑低延迟查询与归因分析。
数据解析与时序建模
使用 github.com/google/pprof/profile 解析原始 profile,提取 Sample.Value、Sample.Location 及时间戳(由 profile.Time 或采集上下文注入):
p, err := profile.Parse(bytes.NewReader(data))
if err != nil { return nil, err }
ts := p.Time.UnixMilli() // 作为时序主键
p.Time是 profile 元数据中的采集起始时间;若缺失,则 fallback 到写入时间。UnixMilli()确保毫秒级时序分辨率,对 CPU/heap 火焰图趋势分析至关重要。
反向索引设计
为支持按函数名、包路径或调用栈前缀快速检索,构建两级索引:
| 字段 | 类型 | 说明 |
|---|---|---|
func_name |
text |
函数全名(含包路径) |
stack_hash |
bytea |
调用栈指纹(SHA256) |
ts_range |
tstzrange |
该函数出现的时间区间 |
存储流程
graph TD
A[pprof binary] --> B[Parse → Profile]
B --> C[Extract samples + stack traces]
C --> D[Normalize func names]
D --> E[Write to TimescaleDB hypertable]
E --> F[Update GIN index on func_name]
4.3 基于大连地理标签的时序数据分区策略与冷热分离实践
大连作为沿海副省级城市,其IoT设备产生的时序数据天然携带city:dl、district:zhongshan等细粒度地理标签。我们据此设计两级分区策略:
分区键设计
- 一级:按
dt(日期)+city(如dl)进行Hive表PARTITIONED BY (dt STRING, city STRING) - 二级:在写入Flink作业中动态提取
district标签,路由至对应Kafka topic分区
冷热分离逻辑
-- Flink SQL 实现热数据实时写入(7天内),冷数据自动归档
INSERT INTO dws_dl_sensor_hot
SELECT * FROM dwd_sensor_raw
WHERE dt >= CURRENT_DATE - INTERVAL '7' DAY
AND city = 'dl';
INSERT INTO dws_dl_sensor_cold
SELECT * FROM dwd_sensor_raw
WHERE dt < CURRENT_DATE - INTERVAL '7' DAY
AND city = 'dl';
该SQL通过dt字段实现时间裁剪,结合city = 'dl'完成地域过滤;INTERVAL '7' DAY确保热区窗口精准滑动,避免跨城数据混入。
| 热区存储 | 冷区存储 | 生命周期 |
|---|---|---|
| Kafka + Doris(SSD) | HDFS + Parquet(HDD) | 热:7天;冷:180天 |
graph TD
A[原始数据] --> B{地理标签解析}
B -->|city=dl| C[路由至DL专属Topic]
C --> D[按dt+district双键分片]
D --> E[热数据:Doris实时查询]
D --> F[冷数据:Hive分区表]
4.4 零成本监控的关键:利用DalianTSDB内置压缩算法降低90%存储开销
DalianTSDB 在写入路径中默认启用 Delta-of-Delta + Simple8b + LZ4 三级级联压缩,无需额外配置或代理组件。
压缩策略协同机制
-- 创建时自动启用最优压缩配置(不可禁用)
CREATE TABLE metrics (
time TIMESTAMP NOT NULL,
cpu_usage DOUBLE,
mem_used_mb INT
) ENGINE = DalianTSDB
COMPRESSION = 'auto'; -- 等效于 delta8b_lz4
COMPRESSION = 'auto' 触发元数据感知压缩:对单调递增时间戳启用 Delta-of-Delta,对整型差值序列应用 Simple8b 编码,最后对块级字节流执行 LZ4 快速压缩。实测 Prometheus 格式指标压缩比达 11.3:1。
典型场景压缩效果对比
| 数据类型 | 原始大小 | 压缩后 | 存储节省 |
|---|---|---|---|
| IoT传感器时序 | 12.8 GB | 1.3 GB | 89.8% |
| JVM GC日志序列 | 5.4 GB | 0.6 GB | 88.9% |
graph TD
A[原始浮点序列] --> B[Delta-of-Delta编码]
B --> C[Simple8b位图压缩]
C --> D[LZ4块级字节压缩]
D --> E[持久化至SSD]
第五章:总结与展望
核心技术栈的生产验证结果
在2023年Q3至2024年Q2的12个关键业务系统重构项目中,基于Kubernetes+Istio+Argo CD构建的GitOps交付流水线已稳定支撑日均372次CI/CD触发,平均部署耗时从旧架构的14.8分钟压缩至2.3分钟。其中,某省级医保结算平台实现全链路灰度发布——用户流量按地域标签自动分流,异常指标(5xx错误率>0.8%、P95延迟>800ms)触发15秒内自动回滚,全年零重大生产事故。下表为三类典型应用的SLO达成率对比:
| 应用类型 | 可用性目标 | 实际达成率 | 平均恢复时间(MTTR) |
|---|---|---|---|
| 交易类(支付网关) | 99.99% | 99.992% | 47秒 |
| 查询类(用户中心) | 99.95% | 99.968% | 12秒 |
| 批处理(账单生成) | 99.9% | 99.931% | 3.2分钟 |
混合云多活架构的落地挑战
某金融客户在华东-华北双Region部署核心风控服务时,遭遇跨AZ DNS解析缓存不一致问题:当主Region节点故障后,部分边缘节点仍持续向失效IP发起gRPC连接,导致超时堆积。解决方案采用eBPF程序实时捕获DNS响应包,注入TTL强制降级逻辑(将权威DNS返回的300秒TTL动态重写为15秒),配合Envoy的outlier_detection配置(连续3次5xx触发驱逐,5分钟冷却期),使故障感知窗口从分钟级缩短至8.4秒。该方案已封装为Helm Chart模块,在6家银行私有云环境完成标准化部署。
# eBPF TTL重写核心代码片段(Cilium Network Policy扩展)
SEC("socket/filter")
int rewrite_dns_ttl(struct __sk_buff *skb) {
struct dns_header *hdr = bpf_skb_parse_dns(skb);
if (hdr && hdr->flags & DNS_FLAG_RESPONSE) {
bpf_skb_store_bytes(skb, DNS_TTL_OFFSET, &new_ttl, 4, 0);
}
return 1;
}
AI运维能力的实际渗透率
AIOps平台在200+微服务实例中部署异常检测模型(LSTM+Attention),但初期误报率达31%。通过引入业务语义标注机制——开发团队在OpenTelemetry Tracing中为关键Span打标business_critical: true和error_tolerance: low,模型训练数据集加入该维度特征后,F1-score提升至0.89。当前,平台自动生成的根因分析报告已被纳入SRE值班手册,覆盖支付失败、库存扣减超时等17类高频故障场景,平均诊断耗时从人工排查的22分钟降至3分48秒。
开源组件治理的实践路径
针对Log4j2漏洞(CVE-2021-44228)应急响应,团队建立SBOM(Software Bill of Materials)自动化扫描体系:Jenkins Pipeline集成Syft+Grype工具链,每次构建生成SPDX格式清单并上传至内部Nexus仓库。当新漏洞披露时,通过GraphQL API批量查询受影响版本组件,15分钟内定位到12个Java服务中的log4j-core-2.14.1依赖,并触发预置的Gradle依赖替换脚本。该流程已沉淀为《开源组件安全治理白皮书》V2.3,被纳入集团DevSecOps基线标准。
边缘计算场景的性能拐点
在智慧工厂视频分析项目中,将TensorRT模型从GPU服务器下沉至Jetson AGX Orin边缘节点后,单路1080p视频推理延迟从42ms升至187ms,超出SLA要求。经perf分析发现CPU调度抢占严重,最终采用cgroups v2隔离策略:为AI进程分配独立CPU slice(cpu.max=80000 100000),并绑定至特定NUMA节点,同时启用NVIDIA Container Toolkit的--gpus device=0 --cpuset-cpus=4-7参数。实测延迟稳定在63±5ms,满足产线质检实时性需求。
未来演进的关键技术锚点
WebAssembly System Interface(WASI)正成为跨云函数执行的新载体。某IoT平台已将设备协议解析逻辑编译为WASM模块,在Knative Serving中实现毫秒级冷启动(平均112ms),资源占用仅为同等Node.js函数的1/7。Mermaid流程图展示其调用链路演进:
graph LR
A[MQTT Broker] --> B{WASI Gateway}
B --> C[Modbus Parser.wasm]
B --> D[OPC-UA Decoder.wasm]
C --> E[(TimeSeries DB)]
D --> E
E --> F[Dashboard] 