昨天我們透過 ClickHouse Exporter 了解了 Factory Pattern 如何統一管理不同 signal 的建立,以及 start() 和 shutdown() 如何處理 exporter 的生命週期。
今天,就讓我們來看當一筆 OTLP 格式的資料進入 collector 後,exporter 如何將它轉換成 ClickHouse 可以寫入的格式?回顧昨天提到的 createMetricExporter
函式,裡面剛好就有呼叫到核心的資料轉換函式:
return exporterhelper.NewMetrics(
ctx, set, cfg,
exp.pushMetricsData,
// ...
)
這個 pushMetricsData
就是資料轉換的核心。今天我們將深入這個函式來了解:
由於各個 signal 所包含的資訊不盡相同,因此在今天的原始碼介紹中,我們會以 metrics 作為範例。
pushMetricsData
來看轉換流程當 collector 收到一批 metrics 資料時,collector 便會呼叫 pushMetricsData
這個函式,這個函式傳入的參數有兩個,分別是用來控制超時和取消操作的 ctx
以及型別為 pmetric.Metrics
的參數 md
,是 OTLP 格式 metrcis 資料。
func (e *metricsExporter) pushMetricsData(ctx context.Context, md pmetric.Metrics) error
在深入轉換邏輯前,我們需要先理解 OTLP metrics 的層次結構。pmetric.Metrics 是一個多層巢狀的資料結構:
MetricsData
└── ResourceMetrics (repeated)
├── Resource (資源屬性,如 service.name)
└── ScopeMetrics (repeated)
├── Scope (instrumentation scope)
└── Metric (repeated)
├── Name (metric 名稱)
├── Description
├── Unit
└── Data (根據 metric type 不同)
├── Gauge
├── Sum
├── Histogram
├── ExponentialHistogram
└── Summary
這樣的資料結構所代表的意義為:
如同前面提過,columnar database的 table 是扁平(flat)的,而 ClickHouse 也不例外。因此,pushMetricsData 的主要工作就是把 OTLP 這個巢狀架構的資料給展開,符合 ClickHouse table 的架構:
func (e *metricsExporter) pushMetricsData(ctx context.Context, md pmetric.Metrics) error {
metricsMap := metrics.NewMetricsModel(e.tablesConfig, e.cfg.database())
for i := 0; i < md.ResourceMetrics().Len(); i++ {
metrics := md.ResourceMetrics().At(i)
resAttr := metrics.Resource().Attributes()
for j := 0; j < metrics.ScopeMetrics().Len(); j++ {
rs := metrics.ScopeMetrics().At(j).Metrics()
scopeInstr := metrics.ScopeMetrics().At(j).Scope()
scopeURL := metrics.ScopeMetrics().At(j).SchemaUrl()
for k := 0; k < rs.Len(); k++ {
r := rs.At(k)
var errs error
//exhaustive:enforce
switch r.Type() {
case pmetric.MetricTypeGauge:
errs = errors.Join(errs, metricsMap[pmetric.MetricTypeGauge].Add(resAttr, metrics.SchemaUrl(), scopeInstr, scopeURL, r.Gauge(), r.Name(), r.Description(), r.Unit()))
case pmetric.MetricTypeSum:
errs = errors.Join(errs, metricsMap[pmetric.MetricTypeSum].Add(resAttr, metrics.SchemaUrl(), scopeInstr, scopeURL, r.Sum(), r.Name(), r.Description(), r.Unit()))
case pmetric.MetricTypeHistogram:
errs = errors.Join(errs, metricsMap[pmetric.MetricTypeHistogram].Add(resAttr, metrics.SchemaUrl(), scopeInstr, scopeURL, r.Histogram(), r.Name(), r.Description(), r.Unit()))
case pmetric.MetricTypeExponentialHistogram:
errs = errors.Join(errs, metricsMap[pmetric.MetricTypeExponentialHistogram].Add(resAttr, metrics.SchemaUrl(), scopeInstr, scopeURL, r.ExponentialHistogram(), r.Name(), r.Description(), r.Unit()))
case pmetric.MetricTypeSummary:
errs = errors.Join(errs, metricsMap[pmetric.MetricTypeSummary].Add(resAttr, metrics.SchemaUrl(), scopeInstr, scopeURL, r.Summary(), r.Name(), r.Description(), r.Unit()))
case pmetric.MetricTypeEmpty:
return errors.New("metrics type is unset")
default:
return errors.New("unsupported metrics type")
}
if errs != nil {
return errs
}
}
}
}
return metrics.InsertMetrics(ctx, e.db, metricsMap)
}
可以看到這個函式用了三層迴圈來處理資料,而每一層迴圈都可以對應到 Metrics Data 的架構:
在第三層迴圈裡面有一個 switch case 判斷式,這邊是將不同類型的 metrics 送進不同的函式中,最終資料會寫入不同的 table。這是因為不同 metric type 的資料結構差異很大:
如果資料類型的差異非常大,雖然一樣可以存在同個 table 當中,但可能會導致資料表有過多的 null 值、schema 難以維護以及查詢效能變差等議題。因此對於 Metrics 這個 signal,ClickHouse 採取一種資料類型寫入一張 table 的策略。
而 traces 和 logs 就不像 Metrics 有各種不同的資料型別,因此都只會各自寫入一張 table。
今天我們從 pushMetricsData
函式出發,了解了 ClickHouse Exporter 如何處理 OTLP metrics 資料:
不過,我們還能再更深入的探討,OTLP 的巢狀結構如何轉換成 ClickHouse 的扁平 table?所以,明天我們將以 Gauge 為例,深入分析 table schema 的設計、資料欄位的對應關係,以及這些設計對我們自己在設計 Lakehouse 架構的啟發。
opentelemetry-collector/exporter/exporterhelper