Grafana Tempo の Parquet ファイルの内容を JSON で取り出す

分散トレーシングのバックエンド [Grafana Tempo](https://github.com/grafana/tempo) のストレージはデフォルトで [Apache Parquet](https://parquet.apache.org/) 形式となっています。

今のところ Tempo には、Parquet ファイルの内容をそのまま JSON 化するような機能は見当たらなかったので Go で実装してみました。

# はじめに

Tempo を実行してトレースデータを登録しておきます。

## Tempo 実行

Tempo は [Jaeger](https://github.com/jaegertracing/jaeger) や [Zipkin](https://github.com/openzipkin/zipkin) 等に対応していますが、今回は OpenTelemetry の http プロトコルを使ってトレースを登録するような設定にしてみました。

##### [tempo.yaml](https://github.com/fits/try_samples/tree/master/blog/20230312/tempo_parquet_json/tempo.yaml)

```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 実行例
```shell
$ tempo -config.file tempo.yaml
```

## トレース登録

次にトレースデータを登録します。

OpenTelemetry 用の receiver はポート番号 4318 となっているので、その ```/v1/traces``` へ OpenTelemetry 形式のトレースデータを POST します。

##### トレース登録例
```shell
$ 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
{}
```

トレースの内容はこのようにしました。（内容はかなり適当です）

```json:data/trace1.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）を呼び出します。

[^1]: flush する前の wal 側に出力されている tmp/wal/.../0000000001 ファイルが Parquet ファイルだったりします

##### flush の実行例
```shell
$ curl http://localhost:3200/flush
```

これで、```tmp/blocks/single-tenant/06527bdb-・・・/data.parquet``` ファイルが生成されました。

# Parquet ファイルの JSON 化

data.parquet の内容を JSON で出力する処理を Go で実装します。

Tempo のソースコード [tempodb/encoding/vparquet/schema.go](https://github.com/grafana/tempo/blob/main/tempodb/encoding/vparquet/schema.go) を参考に、Parquet ファイルからデータを取得する項目を定義します。

##### [schema.go](https://github.com/fits/try_samples/tree/master/blog/20230312/tempo_parquet_json/schema.go)
```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](https://github.com/segmentio/parquet-go) の ```ReadFile``` で上記の ```Trace``` を型パラメータに指定すると Parquet ファイルの内容を取り出せます。

あとは ```json.Marshal``` で JSON 化して出力するだけです。

##### [main.go](https://github.com/fits/try_samples/tree/master/blog/20230312/tempo_parquet_json/main.go)
```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))
}
```

##### ビルド例
```shell
$ go build
```

ビルド結果（```tempo_parquet_json```）を data.parquet ファイルに対して実行した結果はこのようになりました。

##### 実行例
```shell
$ ./tempo_parquet_json tmp/blocks/single-tenant/06527bdb-・・・/data.parquet | jq
```
```json
[
  {
    "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"
  },
  ...省略
]
```