分散トレーシングのバックエンド Grafana Tempo のストレージはデフォルトで Apache Parquet 形式となっています。
今のところ Tempo には、Parquet ファイルの内容をそのまま JSON 化するような機能は見当たらなかったので Go で実装してみました。
はじめに
Tempo を実行してトレースデータを登録しておきます。
Tempo 実行
Tempo は Jaeger や Zipkin 等に対応していますが、今回は OpenTelemetry の http プロトコルを使ってトレースを登録するような設定にしてみました。
tempo.yaml
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
http:
storage:
trace:
backend: local
local:
path: ./tmp/blocks
wal:
path: ./tmp/wal
この設定ファイルを使って Tempo を実行します。
Tempo 実行例
$ tempo -config.file tempo.yaml
トレース登録
次にトレースデータを登録します。
OpenTelemetry 用の receiver はポート番号 4318 となっているので、その /v1/traces
へ OpenTelemetry 形式のトレースデータを POST します。
トレース登録例
$ curl http://localhost:4318/v1/traces -H "Content-Type: application/json" -d @data/trace1.json
{}
$ curl http://localhost:4318/v1/traces -H "Content-Type: application/json" -d @data/trace2.json
{}
トレースの内容はこのようにしました。(内容はかなり適当です)
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "sample-app1"
}
}
]
},
"scopeSpans": [
{
"scope": {
"name": "sample-scope"
},
"spans": [
{
"traceId": "00000000000000000000000000000001",
"spanId": "0000000000000001",
"name": "proc1",
"startTimeUnixNano": "1678536011308000000",
"endTimeUnixNano": "1678536021308000000",
"attributes": [
{
"key": "api",
"value": {
"stringValue": "GET /proc1"
}
},
{
"key": "protocol",
"value": {
"stringValue": "http"
}
},
{
"key": "status_code",
"value": {
"stringValue": "200"
}
}
]
},
...省略
]
}
]
}
]
}
このままだと、バックエンド(local)で設定した tmp/blocks
へ Parquet ファイルは出力されていないので 1、Tempo の flush 機能(ポート番号は 3200)を呼び出します。
flush の実行例
$ curl http://localhost:3200/flush
これで、tmp/blocks/single-tenant/06527bdb-・・・/data.parquet
ファイルが生成されました。
Parquet ファイルの JSON 化
data.parquet の内容を JSON で出力する処理を Go で実装します。
Tempo のソースコード tempodb/encoding/vparquet/schema.go を参考に、Parquet ファイルからデータを取得する項目を定義します。
schema.go
package main
...省略
type Resource struct {
Attrs []Attribute
ServiceName string `parquet:",snappy,dict"`
Cluster *string `parquet:",snappy,optional,dict"`
Namespace *string `parquet:",snappy,optional,dict"`
Pod *string `parquet:",snappy,optional,dict"`
Container *string `parquet:",snappy,optional,dict"`
K8sClusterName *string `parquet:",snappy,optional,dict"`
K8sNamespaceName *string `parquet:",snappy,optional,dict"`
K8sPodName *string `parquet:",snappy,optional,dict"`
K8sContainerName *string `parquet:",snappy,optional,dict"`
}
type ResourceSpans struct {
Resource Resource `parquet:""`
ScopeSpans []ScopeSpan `parquet:"ils"`
}
type Trace struct {
TraceID []byte `parquet:""`
ResourceSpans []ResourceSpans `parquet:"rs"`
TraceIDText string `parquet:",snappy"`
StartTimeUnixNano uint64 `parquet:",delta"`
EndTimeUnixNano uint64 `parquet:",delta"`
DurationNanos uint64 `parquet:",delta"`
RootServiceName string `parquet:",dict"`
RootSpanName string `parquet:",dict"`
}
segmentio/parquet-go の ReadFile
で上記の Trace
を型パラメータに指定すると Parquet ファイルの内容を取り出せます。
あとは json.Marshal
で JSON 化して出力するだけです。
main.go
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"github.com/segmentio/parquet-go"
)
func main() {
file := os.Args[1]
rows, err := parquet.ReadFile[Trace](file)
if err != nil {
log.Fatal(err)
}
r, _ := json.Marshal(rows)
fmt.Print(string(r))
}
ビルド例
$ go build
ビルド結果(tempo_parquet_json
)を data.parquet ファイルに対して実行した結果はこのようになりました。
実行例
$ ./tempo_parquet_json tmp/blocks/single-tenant/06527bdb-・・・/data.parquet | jq
[
{
"TraceID": "AAAAAAAAAAAAAAAAAAAAAQ==",
"ResourceSpans": [
{
"Resource": {
"Attrs": [],
"ServiceName": "sample-app1",
"Cluster": null,
"Namespace": null,
"Pod": null,
"Container": null,
"K8sClusterName": null,
"K8sNamespaceName": null,
"K8sPodName": null,
"K8sContainerName": null
},
"ScopeSpans": [
{
"Scope": {
"Name": "sample-scope",
"Version": ""
},
"Spans": [
{
"ID": "AAAAAAAAAAE=",
"Name": "proc1",
"Kind": 0,
"ParentSpanID": "",
"TraceState": "",
"StartUnixNanos": 1678536011308000000,
"EndUnixNanos": 1678536021308000000,
"StatusCode": 0,
"StatusMessage": "",
"Attrs": [
{
"Key": "api",
"Value": "GET /proc1",
"ValueInt": null,
"ValueDouble": null,
"ValueBool": null,
"ValueKVList": "",
"ValueArray": ""
},
{
"Key": "protocol",
"Value": "http",
"ValueInt": null,
"ValueDouble": null,
"ValueBool": null,
"ValueKVList": "",
"ValueArray": ""
},
{
"Key": "status_code",
"Value": "200",
"ValueInt": null,
"ValueDouble": null,
"ValueBool": null,
"ValueKVList": "",
"ValueArray": ""
}
],
"DroppedAttributesCount": 0,
"Events": [],
"DroppedEventsCount": 0,
"Links": "",
"DroppedLinksCount": 0,
"HttpMethod": null,
"HttpUrl": null,
"HttpStatusCode": null
},
{
"ID": "AAAAAAAAAAI=",
"Name": "proc2",
"Kind": 0,
"ParentSpanID": "",
"TraceState": "",
"StartUnixNanos": 1678536031308000000,
"EndUnixNanos": 1678536141308000000,
"StatusCode": 0,
"StatusMessage": "",
"Attrs": [
{
"Key": "api",
"Value": "GET /proc2",
"ValueInt": null,
"ValueDouble": null,
"ValueBool": null,
"ValueKVList": "",
"ValueArray": ""
},
{
"Key": "protocol",
"Value": "http",
"ValueInt": null,
"ValueDouble": null,
"ValueBool": null,
"ValueKVList": "",
"ValueArray": ""
},
{
"Key": "status_code",
"Value": "200",
"ValueInt": null,
"ValueDouble": null,
"ValueBool": null,
"ValueKVList": "",
"ValueArray": ""
}
],
"DroppedAttributesCount": 0,
"Events": [],
"DroppedEventsCount": 0,
"Links": "",
"DroppedLinksCount": 0,
"HttpMethod": null,
"HttpUrl": null,
"HttpStatusCode": null
}
]
}
]
}
],
"TraceIDText": "1",
"StartTimeUnixNano": 1678536011308000000,
"EndTimeUnixNano": 1678536141308000000,
"DurationNanos": 130000000000,
"RootServiceName": "sample-app1",
"RootSpanName": "proc2"
},
...省略
]
-
flush する前の wal 側に出力されている tmp/wal/.../0000000001 ファイルが Parquet ファイルだったりします ↩