概述
相信如今几乎每个开发人员都使用过 Grafana。但大家是否思考过这样一个问题:Grafana 支持各种不同的数据源,每个数据源(Prometheus、MySQL、Loki 和 ElasticSearch 等)都有其独特的数据模型 。那Grafana 的数据源是如何将来自外部服务和 API 的多种数据格式转换为面板可以理解的格式的呢?
数据帧(DataFrame)
为实现这一目标,Grafana 将来自每个数据源的查询结果整合到一个统一的数据结构中,这个数据结构被称为数据帧(Data Fame)。
数据帧是从 R 语言和 Pandas 等分析工具中借用的概念。在 Pandas 中,数据表(Data Table)被称为数据帧(DataFame)。
在 Grafana 中,数据帧是通用数据容器,它既可以表示时序数据(Time Series)也可以表示表数据(Table)。
数据帧(DataFrame)是一种列存储的数据结构,它由元数据(Metadata)和字段(Field) 的集合构成。其中,每一列对应一个字段。每个字段都有其明确地字段类型(FieldType),并支持设置一组可选的标签(Labels) 。
type Frame struct {
// Name is used in some Grafana visualizations.
Name string
// Fields are the columns of a frame.
// All Fields must be of the same the length when marshalling the Frame for transmission.
// There should be no `nil` entries in the Fields slice (making them pointers was a mistake).
Fields []*Field
// RefID is a property that can be set to match a Frame to its originating query.
RefID string
// Meta is metadata about the Frame, and includes space for custom metadata.
Meta *FrameMeta
}
帧类型(Kinds and Formats)
数据有多种类型(Kinds),如时序数据(Time Series Data)、数字(Numeric)、日志(Logs)、直方图(Histogram)等。每种类型都有相应的格式(Formats) ,如宽格式(Wide)、长格式(Long)、多格式(Multi)等。
帧类型(FrameType)的定义包含数据的类型和格式,例如,TimeSeriesWide
表示时序数据的宽格式,类型和格式如下:
- Kind:Time Series
- Format:Wide
时间序列类型(Time Series Kind Formats)
属性
-
帧应按照时间字段升序排序
-
时间字段(Time Field(s) ):
- 不应包含空值
- 字段名称仅用于显示目的,不应包含
labels
- 对于每个帧,第一个时间字段之后的所有额外时间字段都被视为剩余数据
-
值字段(Value Field(s) ):
-
每个数据点(时间,值)的值所在的字段
-
可以包含一个数值或者布尔类型字段,对于数值字段来说:
- 在 Go 中可以是 Float64、* Float64 或 Int64 等
- 在 JS 中可以是 Number
-
值字段的名称通常是时序的名称(__ name __)
-
时间序列的宽格式(TimeSeriesWide)
宽格式(Wide Format)在单个帧中包含一组具有相同时间字段的时间序列。它被称为“宽”,是因为随着更多序列的添加,它会变得更宽。
例如:
Name: Wide
Dimensions: 3 fields by 2 rows
+---------------------+-----------------+-----------------+
| Name: time | Name: cpu | Name: cpu |
| Labels: | Labels: host=a | Labels: host=b |
| Type: []time.Time | Type: []float64 | Type: []float64 |
+---------------------+-----------------+-----------------+
| 2020-01-02 03:04:00 | 3 | 4 |
| 2020-01-02 03:05:00 | 6 | 7 |
+---------------------+-----------------+-----------------+
在宽格式的时间序列数据中,应该具备以下属性:
- 帧中第一个时间类型的字段是所有时序的时间索引。这意味着它为帧内所有时间序列提供了一个共同的时间参照。
- 应该只有一个帧包含数据类型声明。这确保了数据类型的一致性和清晰性。
- 至少应该有一个字段是值字段类型(value FieldType)。值字段存储每个时间点数据值。
- 如果存在多个数值字段(numeric fields),那么时间字段与帧中每个值字段的组合将构成一个时间序列(指标)。
- 时间字段中不应包含重复的值(重复的时间戳)。这样可以确保每个时间点都是唯一的,从而避免数据出现歧义。
剩余数据(Remainder Data):
- 任何没有声明类型或者具有其他类型声明的额外帧被视为剩余数据。
- 帧中的任何字符串字段(string fields)被视为剩余数据。
注意:
时间序列多格式(TimeSeriesMulti)
在多格式(Multi Format)中,每个帧都对应一个时间序列。当数据源的响应中包含多个时间序列,并且这些序列中的时间值不一致时,就必须使用多格式来替代宽格式。多格式之所以被称为“多(multi)”,就是因为它的数据可以横跨多个帧。
例如:
Name: multiExample
Dimensions: 2 Fields by 2 Rows
+-------------------------------+-----------------+
| Name: time | Name: cpu |
| Labels: | Labels: host=a |
| Type: []time.Time | Type: []float64 |
+-------------------------------+-----------------+
| 2020-01-02 03:04:00 +0000 UTC | 3 |
| 2020-01-02 03:05:00 +0000 UTC | 6 |
+-------------------------------+-----------------+
Name: multiExample
Dimensions: 2 Fields by 2 Rows
+-------------------------------+-----------------+
| Name: time | Name: cpu |
| Labels: | Labels: host=b |
| Type: []time.Time | Type: []float64 |
+-------------------------------+-----------------+
| 2020-01-02 03:04:01 +0000 UTC | 4 |
| 2020-01-02 03:05:01 +0000 UTC | 7 |
+-------------------------------+-----------------+
在多格式的时间序列数据中,应该具备以下属性:
- 每个帧至少应包含一个时间字段和一个数值字段。在帧中,每种类型的字段(如时间戳字段或数值字段)首次出现时,将被用于定义或标识时间序列。
- 不同的帧可以有不同长度的字段,也就是说,它们可以包含不同数量的列。然而,在同一帧内,所有列的长度必须相同,以确保数据的一致性。
- 时间字段中不应包含重复的值(重复的时间戳)。
剩余数据(Remainder Data):
- 任何没有声明类型或者具有其他类型声明的额外帧被视为剩余数据。
- 帧中的任何字符串字段(string fields)被视为剩余数据。
注意:
- 多格式(Multi Format)是唯一能够在不进行数据操作的情况下从其他格式转换而来的格式。因此,它是一种可以包含所有其他类型序列信息的格式。
- 在这里可以查看 Go 的示例
时间类型长格式(TimeSeriesLong)
这是一种在类似 SQL 的系统中常见的响应格式。你可以在 Grafana 的文档:Multiple dimensions in table format 查看到一些简单(但不是完整的)示例。目前,它作为某些数据源在后端查询类似 SQL 的数据时的数据转换存在,可以参见这个 Go 语言示例来了解该代码的工作原理。
这种格式被称为长(Long)格式(也称为窄(narrow)格式),因为与宽格式相比,它需要更多的行来保存相同的序列数据,因此它会变得更长。示例如下:
Name: Long
Dimensions: 4 Fields by 4 Rows
+-------------------------------+-----------------+-----------------+------------------+
| Name: time | Name: aMetric | Name: bMetric | Name: host |
| Labels: | Labels: | Labels: | Labels: |
| Type: []time.Time | Type: []float64 | Type: []float64 | Type: []string |
+-------------------------------+-----------------+-----------------+------------------+
| 2020-01-02 03:04:00 +0000 UTC | 2 | 10 | foo |
| 2020-01-02 03:04:00 +0000 UTC | 5 | 15 | bar |
| 2020-01-02 03:05:00 +0000 UTC | 3 | 11 | foo |
| 2020-01-02 03:05:00 +0000 UTC | 6 | 16 | bar |
+-------------------------------+-----------------+-----------------+------------------+
上述的长格式数据可以被转换为宽格式,如下:
Name: Wide
Dimensions: 5 fields by 2 rows
+---------------------+------------------+------------------+------------------+------------------+
| Name: time | Name: aMetric | Name: bMetric | Name: aMetric | Name: bMetric |
| Labels: | Labels: host=foo | Labels: host=foo | Labels: host=bar | Labels: host=bar |
| Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []float64 |
+---------------------+------------------+------------------+------------------+------------------+
| 2020-01-02 03:04:00 | 2 | 10 | 5 | 15 |
| 2020-01-02 03:05:00 | 3 | 11 | 6 | 16 |
+---------------------+------------------+------------------+------------------+------------------+
在宽格式的时间序列数据中,应该具备以下属性:
-
第一个时间字段用作时间戳。
-
时间字段可以包含重复的时间戳,但必须按时间的升序排序。
-
可以有多个字符串字段,对于字符串字段来说:
- 字段的名代表维度名称(例如,时序数据中的 label 名称)。
- 字段的值代表 label 的值。
剩余数据(Remainder Data):
- 任何没有声明类型或者具有其他类型声明的额外帧被视为剩余数据。
- 第一个时间字段之后的任何其他的时间字段。
注意:
- 在这种格式中,维度(例如:host=value)是从字段中的值中提取的,而不是像其他格式那样在字段模式中声明。
日志类型(Logs)
属性和字段
-
Time 字段:必须
- 第一个字段是名称为
timestamp
的时间字段 - 不能为空
- 第一个字段是名称为
-
Body 字段:必须
-
Severity 字段:可选
-
ID 字段:可选
示例:
data.NewFrame(
"logs",
data.NewField("timestamp", nil, []time.Time{time.UnixMilli(1645030244810), time.UnixMilli(1645030247027), time.UnixMilli(1645030247027)}),
data.NewField("body", nil, []string{"message one", "message two", "message three"}),
data.NewField("severity", nil, []string{"critical", "error", "warning"}),
data.NewField("id", nil, []string{"xxx-001", "xyz-002", "111-003"}),
data.NewField("labels", nil, []json.RawMessage{[]byte(`{}`), []byte(`{"hello":"world"}`), []byte(`{"hello":"world", "foo": 123.45, "bar" :["yellow","red"], "baz" : { "name": "alice" }}`)}),
)