xObserve

  1. 图表 Panel
  2. 图表数据和转换

在 Grafana 中,所有的数据都是 DataFrame 格式, 相对应的 xObserve 格式是 SeriesData

DataFrameSeriesData 在格式上几乎一致,但是与 Grafana 不同的是, xObserve 并没有试图去把所有数据格式都统一为 SeriesData, 而是不同的图表有不同的数据格式,例如:

  1. 时间序列数据统一用 SeriesData 格式, 这也是 xObserve 中使用最广的,大部分图表类型都是这个数据格式,例如 Graph, Table, Stats, Gauge等.
  2. NodeGraph 数据格式, 用于展示依赖关系图,包含 nodes 和 edges 信息
  3. Trace 数据格式, 用于链路数据,原生支持 Jaeger

这些不同的数据格式之间并不兼容,如果非要用一个统一的数据格式来表示它们,那么这个数据格式将会变得难以理解和使用。所以我们使用不同的数据格式来表示不同的图表。

对于大部分用户来说,并不需要关心数据格式。因为这些数据格式是在数据源读取到后,自动转换为图表所需的格式。但如果你想用 HTTP 数据源去某个 HTTP API 查询数据,那就要知道使用的图表所需的数据格式。毕竟外部获取的数据格式,肯定不能直接用于 xObserve 的。

但是我们不会详细介绍每个数据格式的细节,而是告诉大家在需要时,该如何找到这种数据格式。

最终数据格式

先给出最终数据格式的定义:最终传给图表直接使用的数据格式,就是最终数据格式

假设你使用 HTTP 数据源从外部的 HTTP API 查询到格式为 A 的数据,那你必须要把 A 格式转换为图表所需的最终数据格式,才能进行展示。

如何找到所需的数据格式

我们以 Graph 为例,来看看如何找到所需的数据格式。

  1. 点击图表标题区域,打开图表菜单并选择 Debug Panel
  2. 选择 Panel Data 标签页, 你应该看到如下的数据格式:
[
  [
    {
      "id": 65,
      "name": "{\"__name__\"=\"go_goroutines\",\"instance\"=\"localhost:9090\",\"job\"=\"prometheus\"}",
      "length": 21,
      "fields": [
        {
          "name": "Time",
          "type": "time",
          "values": [
            1692950385,
            1692950400,
            1692950415,
          ]
        },
        {
          "name": "Value",
          "type": "number",
          "values": [
            34,
            34,
            34,
          ],
          "labels": {
            "__name__": "go_goroutines",
            "instance": "localhost:9090",
            "job": "prometheus"
          }
        }
      ],
      "rawName": "{\"__name__\"=\"go_goroutines\",\"instance\"=\"localhost:9090\",\"job\"=\"prometheus\"}",
      "color": "#73BF69"
    },
    {
      "id": 65,
      "name": "{\"__name__\"=\"go_goroutines\",\"instance\"=\"localhost:9100\",\"job\"=\"node\"}",
      "length": 21,
      "fields": [
        {
          "name": "Time",
          "type": "time",
          "values": [
            1692950385,
            1692950400,
            1692950415,
          ]
        },
        {
          "name": "Value",
          "type": "number",
          "values": [
            7,
            7,
            7,
          ],
          "labels": {
            "__name__": "go_goroutines",
            "instance": "localhost:9100",
            "job": "node"
          }
        }
      ],
      "rawName": "{\"__name__\"=\"go_goroutines\",\"instance\"=\"localhost:9100\",\"job\"=\"node\"}",
      "color": "#FADE2A"
    }
  ]
]

以上就是 Graph 图表所需的最终数据格式,而这个最终数据格式中的每个列表元素,就是开头提到的 SeriesData 格式,简单表示下:

[
  SeriesData1,
  SeriesData2,
  ...
]

NodeGraph 数据格式

下面再来看看 NodeGraph 的最终数据格式。

将图表类型切换为 NodeGraph, 并打开 Panel Debug - Panel Data, 你可以在其中看到 NodeGraph 最终的数据格式如下:

[
  {
    "nodes": [
      {
        "id": "frontend",
        "label": "frontend",
        "data": {
          "success": 0
        },
      },
      {
        "id": "route",
        "label": "route",
        "data": {
          "success": 70
        },
      },
    ],
    "edges": [
      {
        "source": "customer",
        "target": "mysql",
        "label": "7",
        "data": {
          "success": 7
        },
      },
      {
        "source": "frontend",
        "target": "customer",
        "label": "7",
        "data": {
          "success": 7
        },
      }
    ]
  }
]

假设我们通过 HTTP 数据源去查询数据,那你需要把查询到的数据转换成上面的数据格式,才能在 NodeGraph 中展示出来。

图表数据转换

写在前面:无论数据如何转换,都必须符合图表需要的 最终格式,否则图表将无法正常显示。

有时候,从数据源查询到的数据并不符合图表的要求,还有时候,我们需要对数据做一些变换,例如合并变换,这时候就需要对数据进行转换。

因此,我们需要一个方法来对查询到的数据进行修改编辑,为此 xObserve 提供了一个通用的数据转换功能。

下面来看一个例子,我们要将数据中的时间戳字段转换为时间字符串,这样在 Table 中就能看到更友好的时间格式。

将时间戳转换为时间字符串

对于 Table 图表,我们已经知道如何将时间戳字段转换为可读的时间字符串,如果不知道如何转换,请查看 教程文档

现在我们找另一种方法来实现同样的效果。

  1. 创建一个 Table 图表,并使用 TestData 数据源。
  2. 选择 变换 标签页
panel-data-transform
  1. 点击 编辑函数 按钮
  2. 函数代码如下:
function transform(rawData,lodash, moment) {
    for (const d of rawData) {
        for (const series of d) {
            for (const field of series.fields) {
                if (field.type == "time") {
                    const values = []
                    for (const v of field.values) {
                        values.push(moment(v * 1000).format("YY-MM-DD HH:mm::ss"))
                    }
                    field.values = values
                }
            }
        }
    }
    return rawData
}

Table 图表使用的是 SeriesData 时间序列数据格式,这里我们对其中的时间字段值进行了变换,可以看出,变换前的格式是 Table 所需的最终数据格式,而变换后的格式,依然是最终数据格式。

虽然这个例子中,变换前后都是最终数据格式,但其实,变换前不是最终数据格式也很正常,例如如果我们使用HTTP 数据源从外部 HTTP API 查询数据,那查询到的结果大概率并不是我们需要的最终数据格式,只要大家在这个变换函数中,将其转换成最终数据格式即可。

上面的函数并不复杂,但是相当有用,而且是一个很好的例子,但是提交后,你却发现 Table 没有任何变化,依然显示的是时间戳值。

开启变换

原因是我们还需要在图表设置中启用变换功能,否则变换函数不会有任何效果:

transform-option

如上所示,在图表的基础设置中,开启变换选项,这次你会看到 Table 成功显示了时间字符串,不再是时间戳:

transformed-table

总结:非常重要的最终数据格式

最终数据格式在 xObserve 中非常重要,无论是你要对现有的数据进行变换,还是对外部查询来的不兼容数据进行转换,最终目标都是要实现最终数据格式。

如果想知道某个图表类型的最终数据格式也很简单:使用 TestData 作为数据源并选择你需要的图表类型,然后打开 Panel Debug -> Panel Data ,其中的数据格式就是最终数据格式。

最终数据格式的组成

最终数据格式由以下部分组成:

[
    dataOfQueryA,
    dataOfQueryB,
    ...
]

可以看出,最终数据格式是一个列表,里面每一个元素都是一条查询语句查询到的结果。

上面的列表中,有两个数据对象组成,分别是 dataOfQueryAdataOfQueryB,它们分别由图表中定义的查询语句 A 和 B 查询而来。

panel-query

在上图中,你可以清晰看到该图表有两条查询语句,每一条查询语句都可以查询到一个结果,然后插入到最终的结果列表中。实际上,你可以在一个图表中添加任意多个查询语句,但是我们不推荐这么做,因为这会让图表变得比想象的更复杂。

HTTP 数据源查询到的结果

如果是使用 HTTP 数据源从外部 HTTP API 查询数据,那每一条 HTTP 请求都相当于上面图表中的一个查询语句

HTTP 数据源查询到的结果有两种可能性:

  1. 查询到的结果格式不为 xObserve 支持,此时需要在 变换 标签页中,将其转换为最终数据格式。
  2. 查询到的结果格式跟最终数据格式列表中元素格式一致,此时不需要进行变换。

但如果是第二种,可以肯定的是,该接口是专门为 xObserve 开发的数据查询接口,那么问题来了:如果我要为 xObserve 开发原生数据查询接口,到底应该返回什么格式?

为 xObserve 实现原生数据查询接口

首先可以肯定,接口返回的数据格式要跟最终数据格式中的元素格式保持一致。

下面以 NodeGraph 为例,假设在 Panel Debug -> Panel Data 中查看到的最终数据格式如下:

[
  {
    "nodes": [
      {
        "id": "frontend",
        "label": "frontend",
        "data": {
          "success": 0
        },
      },
      {
        "id": "route",
        "label": "route",
        "data": {
          "success": 70
        },
      },
    ],
    "edges": [
      {
        "source": "customer",
        "target": "mysql",
        "label": "7",
        "data": {
          "success": 7
        },
      },
      {
        "source": "frontend",
        "target": "customer",
        "label": "7",
        "data": {
          "success": 7
        },
      }
    ]
  }
]

那么我们的数据查询接口返回的数据格式应该跟这个列表中的元素格式保持一致也就是

{
  "nodes": [
    {
      "id": "frontend",
      "label": "frontend",
      "data": {
        "success": 0
      },
    },
    {
      "id": "route",
      "label": "route",
      "data": {
        "success": 70
      },
    },
  ],
  "edges": [
    {
      "source": "customer",
      "target": "mysql",
      "label": "7",
      "data": {
        "success": 7
      },
    },
    {
      "source": "frontend",
      "target": "customer",
      "label": "7",
      "data": {
        "success": 7
      },
    }
  ]
}

但光这样还不行,一个只返回数据的接口是很不规范的,因此我们还得包上一层,让返回结果更加完整:

{
    "status": "success",
    "error": "error message",
    "data": {
      "nodes": [
        {
          "id": "frontend",
          "label": "frontend",
          "data": {
            "success": 0
          },
        },
        {
          "id": "route",
          "label": "route",
          "data": {
            "success": 70
          },
        },
      ],
      "edges": [
        {
          "source": "customer",
          "target": "mysql",
          "label": "7",
          "data": {
            "success": 7
          },
        },
        {
          "source": "frontend",
          "target": "customer",
          "label": "7",
          "data": {
            "success": 7
          },
        }
      ]
    }
}

可以看到,返回结果中的 data 就是最终数据格式中的一个元素。

思考题

留给大家一个思考题:如果是如下的最终数据格式:

[
  [
    {
      "id": 65,
      "name": "{\"__name__\"=\"go_goroutines\",\"instance\"=\"localhost:9090\",\"job\"=\"prometheus\"}",
      "length": 21,
      "fields": [
        {
          "name": "Time",
          "type": "time",
          "values": [
            1692950385,
            1692950400,
            1692950415,
          ]
        },
        {
          "name": "Value",
          "type": "number",
          "values": [
            34,
            34,
            34,
          ],
          "labels": {
            "__name__": "go_goroutines",
            "instance": "localhost:9090",
            "job": "prometheus"
          }
        }
      ],
      "rawName": "{\"__name__\"=\"go_goroutines\",\"instance\"=\"localhost:9090\",\"job\"=\"prometheus\"}",
      "color": "#73BF69"
    },
    {
      "id": 65,
      "name": "{\"__name__\"=\"go_goroutines\",\"instance\"=\"localhost:9100\",\"job\"=\"node\"}",
      "length": 21,
      "fields": [
        {
          "name": "Time",
          "type": "time",
          "values": [
            1692950385,
            1692950400,
            1692950415,
          ]
        },
        {
          "name": "Value",
          "type": "number",
          "values": [
            7,
            7,
            7,
          ],
          "labels": {
            "__name__": "go_goroutines",
            "instance": "localhost:9100",
            "job": "node"
          }
        }
      ],
      "rawName": "{\"__name__\"=\"go_goroutines\",\"instance\"=\"localhost:9100\",\"job\"=\"node\"}",
      "color": "#FADE2A"
    }
  ]
]

那么它包含了几个查询语句的结果,我们提供的 HTTP API 应该返回什么格式的数据?

答案

可以看出,上述的最终数据列表中,只有一个元素,因此只包含一条查询语句的结果。

HTTP API 返回的内容如下:

{
    "status": "success",
    "error": "error message",
    "data": [
      {
        "id": 65,
        "name": "{\"__name__\"=\"go_goroutines\",\"instance\"=\"localhost:9090\",\"job\"=\"prometheus\"}",
        "length": 21,
        "fields": [
          {
            "name": "Time",
            "type": "time",
            "values": [
              1692950385,
              1692950400,
              1692950415,
            ]
          },
          {
            "name": "Value",
            "type": "number",
            "values": [
              34,
              34,
              34,
            ],
            "labels": {
              "__name__": "go_goroutines",
              "instance": "localhost:9090",
              "job": "prometheus"
            }
          }
        ],
        "rawName": "{\"__name__\"=\"go_goroutines\",\"instance\"=\"localhost:9090\",\"job\"=\"prometheus\"}",
        "color": "#73BF69"
      },
      {
        "id": 65,
        "name": "{\"__name__\"=\"go_goroutines\",\"instance\"=\"localhost:9100\",\"job\"=\"node\"}",
        "length": 21,
        "fields": [
          {
            "name": "Time",
            "type": "time",
            "values": [
              1692950385,
              1692950400,
              1692950415,
            ]
          },
          {
            "name": "Value",
            "type": "number",
            "values": [
              7,
              7,
              7,
            ],
            "labels": {
              "__name__": "go_goroutines",
              "instance": "localhost:9100",
              "job": "node"
            }
          }
        ],
        "rawName": "{\"__name__\"=\"go_goroutines\",\"instance\"=\"localhost:9100\",\"job\"=\"node\"}",
        "color": "#FADE2A"
      }
    ]
}