🪪

Prometheus メトリクスのダッシュボードを Perses で作成する

2024/12/01に公開

概要

Perses とは

Perses は prometheus メトリクスを可視化するダッシュボードをコードから作成する Dashboard as Code (DaC) の概念を実現するための OSS です。

https://perses.dev/

最初のコミットが 2021/1, 現在の star 数は ~ 900 程と比較的歴史の浅いプロジェクトですが、2024/8 には CNCF の sandbox プロジェクトに採択されました。PromCon Europe 2024 についた書かれた 2024/10 の CNCF ブログでも、開発初期段階ではあるが prometheus のネイティブなダッシュボードとして発展が期待されていると語られています。

https://www.cncf.io/blog/2024/10/17/prometheus-3-0-unveiled-highlights-from-promcon-europe-2024/

On the visualization side, one of the interesting developments coming out of PromCon this year was Perses project that recently joined the CNCF. Julius hinted that while Grafana has been the default for many, Perses is offering a lightweight, Prometheus-native dashboarding experience, with GitOps capabilities and foundational open source philosophy. Perses is still in its early days, but it’s worth keeping an eye on as it evolves.

perses は Grafana と同様に prometheus メトリクスをフィルタリング、集計してダッシュボード形式で表示できる機能や、ダッシュボード内のパネルを柔軟にカスタマイズできる機能があります。そしてこれらのダッシュボードをコードから生成できる点が最大の特徴になっています。

モチベーション

とはいえ prometheus で収集したメトリクスを可視化するためのツールといえば Grafana が定番なので、わざわざ perses を新しく使用する必要性やメリットは何なのかという点がまず疑問として上がってくるかと思います。これに関しては Promcon について 2023/11 に書かれた CNCF ブログ内で、prometheus メトリクスを可視化するためのツールとして perses のような DaC を導入するに至るモチベーションが書かれています。

https://www.cncf.io/blog/2023/11/11/promcon-recap-unveiling-perses-the-gitops-friendly-metrics-visualization-tool/

内容を簡単にまとめると以下のようになります。

  • Grafana をアップグレードすると、スキーマの変更により既存のダッシュボードの表示が壊れることがよくあった。ダッシュボードの数が多い大規模な環境(ブログ内では 5000 以上とのこと)ではこれらを手動で修正するのは大きな負担となる。
  • 上記の課題を解決するために、GitOps の手法や CI/CD と統合してコードからダッシュボードを作成できるようになれば効果的。
  • Grafana ダッシュボードのデータ構造では IaC のように扱うのが難しいため、別のツールで実現する必要がある。

Grafana のダッシュボードやパネルの実態は json なので IaC や CI/CD をやろうと思えばできるかもしれませんが、確かに既存の仕組みではデプロイの自動化やプロパティの検証が難しい面があります。
例えば terraform では grafana 公式の grafana provider が提供されていますが、datasource や folder といったリソースは HCL の構文で記述できるのに対し dashboard は json の中身に相当する内容を encode して作成するようになっています。

resource "grafana_folder" "test" {
  title = "My Folder"
  uid   = "my-folder-uid"
}

resource "grafana_dashboard" "test" {
  folder = grafana_folder.test.uid
  config_json = jsonencode({
    "title" : "My Dashboard",
    "uid" : "my-dashboard-uid"
  })
}

ダッシュボード内に含まれるパネルが多くなってくると json も数千行レベルになってくるので、各プロパティの値を検証したり厳密に管理していくのはなかなかつらいものがあります。このような背景もあってダッシュボードも GitOps のオペレーションに組み込んで管理できる DaC の必要性が増してきたということが読み取れます。

その他にブログでは grafana の管理団体である grafana Lab が grafana や loki 等のライセンスを Apache License v2.0 から AGPLv3 に変更した点についても言及しています。

The foundational open source strategy is of particular importance to the community in light of Grafana’s relicensing last year from Apache 2.0 to the more restrictive and copyleft open source license AGPLv3, and the uncertainty of what other changes the future holds. This community concern around the Grafana relicensing triggered the foundation of CoreDash Project under the CNCF, seeking Apache 2.0 foundational open source alternatives. As a CNCF Ambassador, I’m particularly excited to see Perses targeting joining the CNCF to fill in this gap in the CNCF observability stack.

grafana が将来的にどのようになるかは不明ですが、オープンソースとして利用できる製品をコミュニティで維持していくという精神も perses の開発のモチベーションに関わっていそうです。これを意識してか、perses のドキュメントでは 100 % open source と明記されています。

Open Source#
Perses is 100% open source and community-driven. All components are available under the Apache 2.0 License on GitHub.

Perses is a Cloud Native Computing Foundation sandbox project.

Dashboard as Code

perses では cue という(謎の)言語と Golang の 2 つでコードを記述することができます。

ダッシュボードの定義やプロパティをコードで記述し、ダッシュボードのリソース定義 (yaml) をコードから生成してデプロイするという方法で DaC を実現します。プログラミング言語でコードを記述してリソースの定義を生成するという点では aws CDKpulumi に近いイメージとなっています。

perses のリソース

perses は grafana と同様にダッシュボードを管理するツールであるため、perses 内で定義されているリソースの多くは grafana と同じような概念となっています。今回の記事で扱う perses リソースについて以下で簡単に記載します。

project

project はワークスペースに相当する概念で、この中で複数のダッシュボードを管理します。

datasource

grafana の datasource とほぼ同じで、ダッシュボードに表示するメトリクスの参照先のデータソースを表します。現時点では prometheus しか指定できないようですが、データソースの仕様は plugin として定義されているため、将来的には他のデータソースやユーザーが独自に定義したデータソースも参照できるようになることが期待されます。

dashboard

grafana の dashboard とほぼ同じで、この中に複数のパネルを作成してメトリクスを可視化します。ダッシュボード内の panel のレイアウトが自由に変えられる点も grafana と同じ。

panel

panel は時系列グラフや棒グラフ、table といった 1 つのグラフを描写するオブジェクトに該当します。これも grafana の panel とほぼ同じ。

user

user は perses にリソースを作成したり web UI にアクセスする際の認証として使用されます。これも grafana の user とほぼ同じ。user がどのような操作を実行できるかといった権限は k8s と同じように記述できる rolerolebinding を使った RBAC で管理します。

使ってみる

docker で perses をインストールする

https://perses.dev/ の記載の通り perses は kubernetes-native に作られているので k8s クラスタ上にインストールできます。ドキュメントのインストール手順 には記載されていませんがクラスタ上にデプロイするための helm chart も用意されています。ただ試してみたところ、現時点では chart の values.yml で perses の構成ファイルのすべての項目を設定できるわけではないようです。

また、ドキュメントの通り operator pattern での管理も想定されています。

Use the Perses operator to manage your Perses deployments & dashboards as CRDs. Leverage on the datasource discovery to retrieve data from your datasource pods/services.

operator は perses/perses-operator で開発されていますが、これを k8s 上にデプロイするための helm chart は現時点ではまだなさそうです。

というわけで、今回は perses のコンテナイメージを利用して docker compose でインストールします。
perses に関する構成は単一の yaml ファイル内で記述して perses を実行する際の --config で指定することで適用します。構成ファイルに設定可能な項目は configuration にまとめられていますが、プロパティによってはそれほど詳細な説明がないものあるのでひとまず helm chart でインストールした際に自動生成される config を参考に設定します。

config.yml
security:
  readonly: false
  enable_auth: true
  cookie:
    same_site: lax
    secure: false
  authentication:
    access_token_ttl: 24h
    refresh_token_ttl: 24h
    providers:
      enable_native: true
  authorization:
    guest_permissions:
      - actions: ["*"]
        scopes: ["*"]
database:
  file:
    extension: "json"
    folder: "/perses"
schemas:
  datasources_path: /etc/perses/cue/schemas/datasources
  interval: 5m
  panels_path: /etc/perses/cue/schemas/panels
  queries_path: /etc/perses/cue/schemas/queries
  variables_path: /etc/perses/cue/schemas/variables

docker-compose.yml では config に上記の config.yml を指定します。

docker-compose.yml
services:
  perses:
    container_name: perses
    image: persesdev/perses:v0.48.0
    ports:
      - 8080:8080
    command:
        - --config=/etc/perses/config/config.yml
        - --web.listen-address=:8080
        - --web.hide-port=false
        - --web.telemetry-path=/metrics
        - --log.level=info
        - --log.method-trace=true
    volumes:
      - type: bind
        source: ./config.yml
        target: /etc/perses/config/config.yml
      - ./secret:/etc/perses/config

perses コンテナを起動。

docker compose up -d

CLI インストール

perses にリソースをデプロイする際は perses の API を実行することもできますが、デプロイを簡単に行うための perses CLI が用意されています。デプロイは基本的に perses CLI を通じて行うので https://perses.dev/perses/docs/user-guides/dashboard-as-code/ に沿って perses の release からインストールしておきます。

wget https://github.com/perses/perses/releases/download/v0.48.0/perses_0.48.0_linux_amd64.tar.gz
tar zxvf perses_0.48.0_linux_amd64.tar.gz
sudo mv percli /usr/local/bin

コードを記述する言語に cue を使用する場合は cue が必要になるのでインストールします。

wget https://github.com/cue-lang/cue/releases/download/v0.11.0/cue_v0.11.0_linux_amd64.tar.gz
tar zxvf cue_v0.11.0_linux_amd64.tar.gz
sudo mv cue /usr/local/bin

cue を実行する際は go も必要になるのでこちらもインストール。

wget https://go.dev/dl/go1.23.3.linux-amd64.tar.gz
tar zxvf go1.23.3.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.23.3.linux-amd64.tar.gz
rm -rf go go1.23.3.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.zshrc
source ~/.zshrc

prometheus の準備

grafana と同様にデータソースとなる prometheus は perses に同梱されていないので別途作成する必要があります。取得するメトリクスは何でもいいですが、ここでは Prometheus, Thanos による k8s クラスタ Metrics の記事を参考に k8s クラスタの node, pod メトリクスを収集するように設定しておきます。

セットアップ

DaC でダッシュボードを作れるようになるまでにいくつか準備が必要なのでここでまとめて行います。
まずはじめに、perses を起動した段階ではまだユーザーが存在していないので User リソースで適当なユーザーを作成します。

user.yml
kind: User
metadata:
  # User name for login
  name: test
spec:
  firstName: test # Optional
  lastName: test # Optional
  nativeProvider:
    password: test # Optional

json に変換したのち、user create API を使ってデプロイ。

$ cat user.yml | yq -P > user.json
$ curl -H "content-type: application/json" -d @user.json http://0.0.0.0:8080/api/v1/users
{"kind":"User","metadata":{"name":"test","createdAt":"2024-11-27T16:11:27.308967779Z","updatedAt":"2024-11-27T16:11:27.308967779Z","version":0},"spec":{"firstName":"test","lastName":"test","nativeProvider":{"password":"\u003csecret\u003e"}}}

これで percli login で作ったユーザーの認証情報でログインできるようになります。

$ percli login http://0.0.0.0:8080 --username test --password test
successfully logged in http://0.0.0.0:8080

ログインした際の認証情報は ~/.perses/config.json に保存されます。

{"rest_client_config":{"url":"http://0.0.0.0:8080","authorization":{"type":"Bearer","credentials":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNzMyNzI0MzI4LCJuYmYiOjE3MzI3MjM0Mjh9.yzoz4vwtXjTHA5OQ_tuQEWqdAVDrtZpcOEznC0aO9U2YkKFKfH2SUqRptUDM7Q7gE2eHv98w5-glIch9KIkPGA"},"tls_config":{"insecureSkipVerify":false}},"refresh_token":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNzMyODA5ODI4LCJuYmYiOjE3MzI3MjM0Mjh9.0hO6zJ4oQl24ScQww7UiaAT4_1j0Lz6thbCmB9_rH1EC5XfmsNHlGHnnrOWo0KCRRd8nCF_XI2AXcQTGMLxm8A","dac":{}}%

API を直接実行する際はこの中の credentials を token として使用できます。ただ perses CLI で必要な操作は実行できるのあまり使用する機会はないかも。

$ export TOKEN=$(cat ~/.perses/config.json  | yq -r ".rest_client_config.authorization.credentials")

# 登録済みユーザーを API で取得
$ curl -sSH "Authorization: Bearer $TOKEN" http://0.0.0.0:8080/api/v1/users | yq -P
- kind: User
  metadata:
    name: test
    createdAt: "2024-11-29T13:16:51.549195678Z"
    updatedAt: "2024-11-29T13:16:51.549195678Z"
    version: 0
  spec:
    firstName: test
    lastName: test
    nativeProvider:
      password: <secret>

次に作業用の project を作成します。本格的な運用では用途ごとに project を分割するのが推奨されますが、検証では project が 1 つあれば充分なので以下の test-project で作業していきます。

project.yml
kind: "Project"
metadata:
  name: test-project

いったんリソース作成権限を持つユーザーで認証すれば percli apply でリソースをデプロイできるようになるので今後はこちらを利用します。

$ percli apply -f project.yml
object "Project" "test-project" has been applied

ダッシュボードを作る

設定が一通り完了したので、ここからコードを書いて DaC のアプローチでダッシュボードを作っていきます。コードを書く言語に cue か go のどちらを使うべきかについて特に記述などは見当たりませんでしたが、user guide によると将来的に go SDK ではすべてのプラグインがサポートされない可能性があるとのことです。

Just pick your favorite to start with DaC. If you don't have one, give a try to both!

Note

CUE is the language used for the data model of the plugins, which means you'll always be able to include any external plugin installed in your Perses server into your code when using the CUE SDK.

However, the Golang SDK may not support all the plugins: it's basically up to each plugin development team to provide a Go package to enable the DaC use case.

This statement applies also to any other language we might have a SDK for in the future.

試しに使う分にはどちらでも問題はなさそうなのでここでは cue を使ってコードを書いていきます。

ダッシュボードの作成

まずはじめに https://perses.dev/perses/docs/user-guides/dashboard-as-code/#repository-setup にそって cue module を初期化します。

cue mod init

次に perses 関連のモジュールをインストール。

percli dac setup --version 0.48.0

これにより cue.mod ディレクトリ配下に perses のモジュールが配置されます。


まずは cue でのコードの書き方も合わせて簡単なダッシュボードを作ってみます。input.cue を参考に以下のような my_dashboard.cue を作成。

my_dashboard.cue
package myDaC

import (
    dashboardBuilder "github.com/perses/perses/cue/dac-utils/dashboard"
    prometheusDs "github.com/perses/perses/cue/schemas/datasources/prometheus:model"
)

// ダッシュボードを作る際は基本的に dashboardBuilder を利用
dashboardBuilder & {
   #name: "mytest"
   #display: name: "Containers monitoring"
   #project:   "test-project"
   #datasources: {myPromDemo: {
    default: true
    plugin: prometheusDs & {spec: {
        directUrl: "http://192.168.3.207:31808" // prometheus の URL
    }}
   }}
   #duration:        "3h"
   #refreshInterval: "30s"
}

次に percli dac build コマンドで cue コードからリソース定義の yaml を生成します。

$ percli dac build -f my_dashboard.cue
Succesfully built my_dashboard.cue at built/my_dashboard_output.yaml

書いたコードでエラー等が発生しなければ cue コードからリソースの yaml を生成したものが built ディレクトリに出力されます。このときのディレクトリ構造は以下。

.
├── built
│   └── my_dashboard_output.yaml
├── cue.mod
│   ├── module.cue
│   ├── pkg
│   │   └── github.com
│   │       └── perses
│   │           └── (perses の module)
│   └── usr
└── my_dashboard.cue

できた yaml を見てみると dashboard リソースの定義に合うように各プロパティが設定されていることがわかります。

my_dashboard_output.yaml
kind: Dashboard
metadata:
  name: mytest
  createdAt: "0001-01-01T00:00:00Z"
  updatedAt: "0001-01-01T00:00:00Z"
  version: 0
  project: test-project
spec:
  display:
    name: Containers monitoring
  datasources:
    myPromDemo:
      default: true
      plugin:
        kind: PrometheusDatasource
        spec:
          directUrl: http://192.168.3.207:31808
  panels: {}
  layouts: []
  duration: 3h
  refreshInterval: 30s

リソース定義を perses にデプロイするには percli apply -f [file] を実行します。

$ percli apply -f built/my_dashboard_output.yaml
object "Dashboard" "mytest" has been applied in the project "test-project"

これで perses 上にダッシュボードが作成されたので実際に perses の UI から確認してみます。ブラウザで http://0.0.0.0:8080 にアクセスし、先ほど作った test ユーザーでログインすると以下のように test-project 配下にダッシュボードが作成されていることが確認できます。

ただ上記の cue コードではダッシュボードに panel を追加していないので中身を見ても何もありません。次は prometheus メトリクスを可視化するための panel を追加するように cue コードを修正していきます。

パネルの追加

パネルは grafana におけるパネル とほぼ同じで、promQL のクエリで取得した prometheus メトリクスを様々な形式で表示することができます。ここでは k8s クラスタを構成するノードのメモリ使用量を表示するパネルを作っていきます。

パネルを作成するには基本的に組み込みの panelBuilder を使用します。panelBuilder では panel のタイトルやダッシュボード上での位置、パネル内にどのようなデータを描写するかといった情報を定義します。こちらも input.cue の記述例を参考に設定。

#memoryPanel: panelBuilder & {
 spec: {
  display: name: "Node memory utilization"
  plugin: timeseriesChart & {
   spec: querySettings: [
    {
     queryIndex: 0
     colorMode:  "fixed-single"
     colorValue: "#0be300"
    },
   ]
  }
  queries: [
   {
    kind: "TimeSeriesQuery"
    spec: plugin: promQuery & {
     spec: query: "node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes"
    }
   },
  ]
 }
}

パネル内に表示するデータは queries に指定します。時系列データを表示する際は TimeSeriesQuery を指定。
spec.plugin には promQL のクエリに対応する plugin promQuery を指定し、spec.query にデータを取得するためのクエリ文を記載します。

上記で定義した memoryPanel を dashboardBuilder 内の panelGroups.input に含めることでダッシュボードにパネルを追加できます。全体の cue コードは以下。

my_dashboard_panel.cue
package myDaC

import (
    dashboardBuilder "github.com/perses/perses/cue/dac-utils/dashboard"
    prometheusDs "github.com/perses/perses/cue/schemas/datasources/prometheus:model"
    panelGroupsBuilder "github.com/perses/perses/cue/dac-utils/panelgroups"
    panelBuilder "github.com/perses/perses/cue/dac-utils/prometheus/panel"
    timeseriesChart "github.com/perses/perses/cue/schemas/panels/time-series:model"
    promQuery "github.com/perses/perses/cue/schemas/queries/prometheus:model"
)

dashboardBuilder & {
   #name: "mytest"
   #display: name: "Containers monitoring"
   #project:   "test-project"
   #datasources: {myPromDemo: {
    default: true
    plugin: prometheusDs & {spec: {
        directUrl: "http://192.168.3.207:31808"
    }}
   }}
   #duration:        "3h"
   #refreshInterval: "30s"
   #panelGroups: panelGroupsBuilder & {
   #input: [
   {
    #title: "Resource usage"
    #cols:  3
    #panels: [
     #memoryPanel,
    ]
   },
    ]
   }
 }

#memoryPanel: panelBuilder & {
 spec: {
  display: name: "Node memory utilization"
  plugin: timeseriesChart & {
   spec: querySettings: [
    {
     queryIndex: 0
     colorMode:  "fixed-single"
     colorValue: "#0be300"
    },
   ]
  }
  queries: [
   {
    kind: "TimeSeriesQuery"
    spec: plugin: promQuery & {
     spec: query: "node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes"
    }
   },
  ]
 }
}

perses dac build でビルドした yaml を見ると以下のようになっています。

my_dashboard_panel_output.yaml
kind: Dashboard
metadata:
  name: mytest
  createdAt: "0001-01-01T00:00:00Z"
  updatedAt: "0001-01-01T00:00:00Z"
  version: 0
  project: test-project
spec:
  display:
    name: Containers monitoring
  datasources:
    myPromDemo:
      default: true
      plugin:
        kind: PrometheusDatasource
        spec:
          directUrl: http://192.168.3.207:31808
  panels:
    "0_0":
      kind: Panel
      spec:
        display:
          name: Node memory utilization
        plugin:
          kind: TimeSeriesChart
          spec:
            querySettings:
              - queryIndex: 0
                colorMode: fixed-single
                colorValue: '#0be300'
        queries:
          - kind: TimeSeriesQuery
            spec:
              plugin:
                kind: PrometheusTimeSeriesQuery
                spec:
                  query: node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes
  layouts:
    - kind: Grid
      spec:
        display:
          title: Resource usage
        items:
          - x: 0
            "y": 0
            width: 8
            height: 6
            content:
              $ref: '#/spec/panels/0_0'
  duration: 3h
  refreshInterval: 30s

新しく panels が追加され、kind: Panel で表されるパネルリソースがダッシュボードに追加されました。また、layouts はダッシュボード上のパネルの位置を表しており、各パネルをどの位置にどのくらいのサイズで配置するかといった情報に対応しています。x, y がパネルの座標、width, height がパネルのサイズに対応しますが、このあたりはダッシュボードに設定した col や panel を元に自動で調整してくれます(プロパティで調整することも可能)。

上記の yaml を perses apply でデプロイすると、想定通り先ほど作ったダッシュボードにクラスタ内のノードのメモリ使用量を表す panel が追加されました。

ただこのグラフは y 軸の単位が Byte のままだったり凡例が表示されてなかったりと少々見づらくなっているので、もう少し見やすいようにいくつか調整していきます。

プロパティの追加

パネルの見た目を調整するには cue コード上のプロパティを修正すれば良いですが、どのプロパティがどの設定項目に対応しているかを把握する必要があります。このあたりはいくつか調べ方が考えられますが以下の方法で確認するのが楽です。

まず UI から作ったパネルの Edit を選択すると、以下のように panel の Settings やクエリを編集できるようになります。

グラフが見やすくなるようにいくつか設定を変更した後に json タブを選択すると、変更した値が反映された panel の json の中身が確認できます。これはリアルタイムで反映されるので、どの設定を修正したらどのプロパティが追加・変更されるのかわかります。

今回は y 軸の軸ラベルを表示、単位を Byte に設定。凡例を右側に追加、凡例のフォーマットを k8s ノード IP アドレスに設定、の 4 つの変更を加えましたが、json 内では以下のように追加されています。

変更後の json の一部
      "kind": "TimeSeriesChart",
      "spec": {
        "querySettings": [
          {
            "colorMode": "fixed-single",
            "colorValue": "#0be300",
            "queryIndex": 0
          }
        ],
        "legend": {
          "position": "right"
        },
        "yAxis": {
          "show": true,
          "label": "memory utilization",
          "format": {
            "unit": "bytes"
          }
        },
...
    "queries": [
      {
        "kind": "TimeSeriesQuery",
        "spec": {
          "plugin": {
            "kind": "PrometheusTimeSeriesQuery",
            "spec": {
              "query": "node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes",
              "seriesNameFormat": "{{instance}}"
            }

これより軸の設定は TimeSeriesChart リソースの legend と yAxis、凡例のフォーマットは PrometheusTimeSeriesQuery リソースの seriesNameFormat を設定すれば良さそうということが推測できます。

実際に perses ドキュメントでワード検索すると TimeSeriesChartPrometheusTimeSeriesQuery でそれぞれ上記のプロパティが存在していることがわかるので、これを踏まえた上で cue コードを修正します。

my_dashboard_panel.cue
package myDaC

import (
 dashboardBuilder "github.com/perses/perses/cue/dac-utils/dashboard"
 prometheusDs "github.com/perses/perses/cue/schemas/datasources/prometheus:model"
 panelGroupsBuilder "github.com/perses/perses/cue/dac-utils/panelgroups"
 panelBuilder "github.com/perses/perses/cue/dac-utils/prometheus/panel"
 timeseriesChart "github.com/perses/perses/cue/schemas/panels/time-series:model"
 promQuery "github.com/perses/perses/cue/schemas/queries/prometheus:model"
)

dashboardBuilder & {
 #name: "mytest"
 #display: name: "Containers monitoring"
 #project: "test-project"
 #datasources: {myPromDemo: {
  default: true
  plugin: prometheusDs & {spec: {
   directUrl: "http://192.168.3.207:31808"
  }}
 }}
 #duration:        "3h"
 #refreshInterval: "30s"
 #panelGroups: panelGroupsBuilder & {
  #input: [
   {
    #title: "Resource usage"
    #cols:  2
    #panels: [
     #memoryPanel,
    ]
   },
  ]
 }
}

#memoryPanel: panelBuilder & {
 spec: {
  display: name: "Node memory utilization"
  plugin: timeseriesChart & {
   spec: {
    querySettings: [
     {
      queryIndex: 0
      colorMode:  "fixed-single"
      colorValue: "#0be300"
     },
+    ]
+    legend: {
+     position: "right"
+    }
+    yAxis: {
+     show:  true
+     label: "Memory utilization"
+     format: unit: "bytes"
    }
   }

  }
  queries: [
   {
    kind: "TimeSeriesQuery"
    spec: plugin: promQuery & {
     spec: {
      query: "node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes"
+      seriesNameFormat: "{{instance}}"
     }
    }
   },
  ]
 }
}

再度デプロイすると先ほどのパネルに軸ラベルや凡例がセットされてだいぶ見やすくなりました。

基本的に perses による DaC では以下のような工程を繰り返してダッシュボードを管理していきます。

  1. cue または go でコードを書く
  2. perses dac build でコードをからリソース定義を生成
  3. perses apply でデプロイ
  4. 1 ~ 3 を繰り返して修正・更新

その他の機能

perses ドキュメントでは User Guide がありますが、どのようにダッシュボードを作って管理していくか等の使用例や運用方針については今のところ記載がなさそうで、これからどのように進めていけば良いか分かりづらくなっています。ひとまずドキュメントを参照ながら試した機能を以下に記載していきます。

複数のパネル

上記でパネル 1 つのダッシュボードができたので、今度はノードの CPU, memory, ストレージ使用量を表す 3 つのパネルを追加してみます。

似たようなパネルを複数作成する場合、凡例の位置や軸の設定などいくつか共通する部分をいちいち書いていくのは冗長なので cue の機能を使って共通部分をまとめて管理できるようにします。まずグラフの描写に関して配下のように設定することにします。

  • 凡例の位置は右側に統一する。
  • y 軸の単位はグラフで表示するメトリクスの種類に合わせて変更する。

これを実現するために、cue の definition を使って timeseriesChart のグラフ設定を共通化するための schema timeseriesChartSettings に定義します。今回の範囲ではメトリクスが取りうる単位は percent か byte だけなので unit は 3 つに限定しています。

#timeseriesChartSettings: {
 querySettings: [
  {
   queryIndex: int | *0
   colorMode:  string | *"fixed-single"
   colorValue: string | *"#0be300"
  },
 ]
 legend: {
  position: string | *"right"
 }
 yAxis: {
  format: {
   unit: string | "percent" | "bytes" | "percent-decimal"
  }
 }
}

https://cuelang.org/docs/tour/types/defs/

It’s normal for definitions to specify fields that don’t have concrete values, such as types.

cue の definition では具体的な数値を設定しないのが普通とのことですが、値を指定しない場合に適用される Default Values を設定することはできるので legend 等で指定しています。

メトリクスに関してはいずれも同じ prometheus から参照することがわかっているので、GlobalDatasource を定義してデフォルトのデータソースに指定します(global でなくてもよい)。デフォルトのデータソースを独立したリソースとして作成しておくことで、各ダッシュボードを定義する際に datasource の指定を省略できるようになります。

kind: "GlobalDatasource"
metadata:
  name: prometheus
spec:
  default: true
  plugin:
    kind: PrometheusDatasource
    spec:
      directUrl: http://192.168.3.207:31808

次にノードのストレージ使用率を表示するためのパネルを作ります。timeseriesChart に関しては上記で定義した timeseriesChartSettings の設定を使用し、unit だけ percent を明示的に指定します。query にはストレージ使用率を求めるクエリを指定。

#nodeStoragePanel: panelBuilder & {
 spec: {
  display: name: "Storage utilization"
  plugin: timeseriesChart & {
    spec: #timeseriesChartSettings & {yAxis: format: unit: "percent"}
  }
  queries: [
   {
    kind: "TimeSeriesQuery"
    spec: plugin: promQuery & {
     spec: {
      query:            "(sum(node_filesystem_size_bytes{fstype!='tmpfs', fstype!='overlay'}) by (instance) - sum(node_filesystem_free_bytes{fstype!='tmpfs', fstype!='overlay'}) by (instance) ) / sum(node_filesystem_size_bytes{fstype!='tmpfs', fstype!='overlay'}) by (instance) * 100"
      seriesNameFormat: "{{instance}}"
     }
    }
   },
  ]
 }
}

CPU 使用率を表示するためのパネルでは、以下のクエリでは値が 0 ~ 1 の percent で求められるため unit を percent-decimal に指定します (クエリ内で x100 しても良いですが)。

#nodeCpuUtilizationPanel: panelBuilder & {
 spec: {
  display: name: "CPU utilization"
  plugin: timeseriesChart & {
   spec: #timeseriesChartSettings & {yAxis: format: unit: "percent-decimal"}
  }
  queries: [
   {
    kind: "TimeSeriesQuery"
    spec: plugin: promQuery & {
     spec: {
      query: "1 - (sum(rate(node_cpu_seconds_total{mode='idle'}[1m])) by (instance) / sum(rate(node_cpu_seconds_total[1m])) by (instance))"
      seriesNameFormat: "{{instance}}"
     }
    }
   },
  ]
 }
}

コード全体は以下。

コード全体
my_dashboard_multi.cue
package myDaC

import (
  dashboardBuilder "github.com/perses/perses/cue/dac-utils/dashboard"
  panelGroupsBuilder "github.com/perses/perses/cue/dac-utils/panelgroups"
  panelBuilder "github.com/perses/perses/cue/dac-utils/prometheus/panel"
  timeseriesChart "github.com/perses/perses/cue/schemas/panels/time-series:model"
  promQuery "github.com/perses/perses/cue/schemas/queries/prometheus:model"

)

#timeseriesChartSettings: {
  querySettings: [
    {
      queryIndex: int | *0
      colorMode:  string | *"fixed-single"
      colorValue: string | *"#0be300"
    },
  ]
  legend: {
    position: string | *"right"
  }
  yAxis: {
    format: {
      unit: string | "percent" | "bytes" | "percent-decimal"
    }
  }
}

#nodeStoragePanel: panelBuilder & {
  spec: {
    display: name: "Storage utilization"
    plugin: timeseriesChart & {
      spec: #timeseriesChartSettings & {yAxis: format: unit: "percent"}
    }
    queries: [
      {
        kind: "TimeSeriesQuery"
        spec: plugin: promQuery & {
          spec: {
            query:            "(sum(node_filesystem_size_bytes{fstype!='tmpfs', fstype!='overlay'}) by (instance) - sum(node_filesystem_free_bytes{fstype!='tmpfs', fstype!='overlay'}) by (instance) ) / sum(node_filesystem_size_bytes{fstype!='tmpfs', fstype!='overlay'}) by (instance) * 100"
            seriesNameFormat: "{{instance}}"
          }
        }
      },
    ]
  }
}

#nodeCpuUtilizationPanel: panelBuilder & {
  spec: {
    display: name: "CPU utilization"
    plugin: timeseriesChart & {
      spec: #timeseriesChartSettings & {yAxis: format: unit: "percent-decimal"}
    }
    queries: [
      {
        kind: "TimeSeriesQuery"
        spec: plugin: promQuery & {
          spec: {
            query:            "1 - (sum(rate(node_cpu_seconds_total{mode='idle'}[1m])) by (instance) / sum(rate(node_cpu_seconds_total[1m])) by (instance))"
            seriesNameFormat: "{{instance}}"
          }
        }
      },
    ]
  }
}

#nodeMemoryPanel: panelBuilder & {
  spec: {
    display: name: "Memory utilization"
    plugin: timeseriesChart & {
      spec: #timeseriesChartSettings & {yAxis: format: unit: "bytes"}
    }
    queries: [
      {
        kind: "TimeSeriesQuery"
        spec: plugin: promQuery & {
          spec: {
            query:            "node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes"
            seriesNameFormat: "{{instance}}"
          }
        }
      },
    ]
  }
}

dashboardBuilder & {
  #name:            "node-dashboard"
  #project:         "test-project"
  #duration:        "1h"
  #refreshInterval: "30s"
  #panelGroups: panelGroupsBuilder & {
    #input: [
      {
        #title:  "Node usage"
        #cols:   2
        #height: 10
        #panels: [
          #nodeCpuUtilizationPanel,
          #nodeMemoryPanel,
          #nodeStoragePanel,
        ]
      },
    ]
  }
}

これをデプロイすると、ノード毎の CPU使用率、メモリ使用量、ストレージ使用率の 3 つのパネルが作成されます。

変数の利用

perses には grafana の variable とだいたい同じ概念の variable リソースが定義されており、パネルに表示するメトリクスを動的に変更したりする際に役立ちます。コード内に文字列をハードコーディングして変数として指定する static variable には以下のような種類があります。

この他に prometheus メトリクスに基づいて変数を指定できる variable もいくつか用意されています。

その他、変数には scope の概念があり、単一の project のみで指定可能、複数の project で共通で使用可能といった有効範囲を指定できるようになっています。


変数の機能を使って、k8s クラスタの pod メトリクスを namespace 毎に切り替えて表示できるようにしてみましょう。namespace の一覧に関しては以下のような promQL クエリで取得できます。

count(kube_namespace_status_phase) by (namespace)

実際に prometheus UI 上でクエリを実行すると取得できることが確認できます。

cue コードで変数を指定するには varGroupBuilder を使って独自の変数グループを定義するのが楽です。この中では input 内に独自の変数をリスト形式で指定します。promQLVarBuilder では name に変数名、query に上記のクエリを指定します。

import (
  varGroupBuilder "github.com/perses/perses/cue/dac-utils/variable/group"
  promQLVarBuilder "github.com/perses/perses/cue/dac-utils/prometheus/variable/promql"
)

#myVarsBuilder: varGroupBuilder & {
  #input: [for i in #input {#datasourceName: "prometheus"}]
  #input: [
    promQLVarBuilder & {
      #name:          "namespace"
      #query:         "count(kube_namespace_status_phase) by (namespace)"
    },
  ]
}

これでメトリクスから収集した namespace の一覧が namespace という変数名で参照できるようになります。
クエリ内では $変数名 の表記で変数を参照できます。これ自体はプレースホルダーのようなもので、実際にダッシュボードで表記する際に値が代入されます。

kind: "TimeSeriesQuery"
spec: plugin: promQuery & {
  spec: {
    query: "sum(container_memory_working_set_bytes{container!=\"\", container!=\"POD\", namespace=\"$namespace\"}) by (pod)"
  }

最後に、定義した variable をダッシュボードに含めるには dashboardBuilder の variables に変数を指定します。

dashboardBuilder & {
  #name: "mytest"
  #display: name: "Pod monitoring"
  #project:         "test-project"
  #variables:       #myVarsBuilder.variables
  ...

最終的なコードは以下

コード全文
pod-memory.cue
package myDaC

import (
  dashboardBuilder "github.com/perses/perses/cue/dac-utils/dashboard"
  panelGroupsBuilder "github.com/perses/perses/cue/dac-utils/panelgroups"
  panelBuilder "github.com/perses/perses/cue/dac-utils/prometheus/panel"
  timeseriesChart "github.com/perses/perses/cue/schemas/panels/time-series:model"
  promQuery "github.com/perses/perses/cue/schemas/queries/prometheus:model"
  varGroupBuilder "github.com/perses/perses/cue/dac-utils/variable/group"
  promQLVarBuilder "github.com/perses/perses/cue/dac-utils/prometheus/variable/promql"
)

dashboardBuilder & {
  #name: "mytest"
  #display: name: "Pod monitoring"
  #project:         "test-project"
  #variables:       #myVarsBuilder.variables
  #duration:        "1h"
  #refreshInterval: "30s"
  #panelGroups: panelGroupsBuilder & {
    #input: [
      {
        #title: "Resource usage"
        #cols:  2
        #panels: [
          #memoryPanel,
        ]
      },
    ]
  }
}

#myVarsBuilder: varGroupBuilder & {
  #input: [for i in #input {#datasourceName: "prometheus"}]
  #input: [
    promQLVarBuilder & {
      #name:  "namespace"
      #query: "count(kube_namespace_status_phase) by (namespace)"
    },
  ]
}

#memoryPanel: panelBuilder & {
  spec: {
    display: name: "Pod memory used"
    plugin: timeseriesChart & {
      spec: {
        querySettings: [
          {
            queryIndex: 0
            colorMode:  "fixed-single"
            colorValue: "#0be300"
          },
        ]
        legend: {
          position: "right"
        }
        yAxis: {
          format: {
            unit: "bytes"
          }
        }

      }
    }
    queries: [
      {
        kind: "TimeSeriesQuery"
        spec: plugin: promQuery & {
          spec: {
            query: "sum(container_memory_working_set_bytes{container!=\"\", container!=\"POD\", namespace=\"$namespace\"}) by (pod)"
          }
        }
      },
    ]
  }
}

デプロイするとダッシュボードの左上に設定した namespace 変数が追加されます。以下の図では kube-system が選択されているので、クエリ文の $namespace には kube-system が代入される結果 kube-system 内の pod のメモリ使用量が表示されるようになります。


kube-system namespace 内の pod 毎のメモリ使用量が表示される

値の候補は count(kube_namespace_status_phase) by (namespace) のクエリによって取得された namespace 一覧から選択できます。

namespace を別の値に切り替えると表示されるメトリクスも動的に切り替わります。


metrics namespace 内の pod 毎のメモリ使用量が表示される

このように変数を有効に活用することでクエリの label やフィルタリングを動的に変更できるので、ダッシュボードに表示するデータをより柔軟に表現するのに役立ちます。

ファイルの分割

cue の仕様では go に近い感じで module や package を分割することができます。

ダッシュボード内に含める情報が増えてくると 1 つの cuue ファイルが肥大化してくるので、上記の機能を使ってリソース定義を別ファイルに分割できるか見ていきます。

まず今まで作業していたローカルのディレクトリを module にするため、cue.mod/module.cue の module 名を変更します。cuetorial Modules and Packages によると module 名は github.com のようなドメイン名が一般的とのことなので、ここでは github.com/perses-example に設定します。ただこのドメインはあくまで形式上のものなので実際にコードを github 上にアップロードする必要はないです。

cue.mod/module.cue
- module: "cue.example"
+ module: "github.com/perses-sample"
language: {
   version: "v0.11.0"
}

次に timeseries/settings のディレクトリ内に settings.cue を作成。

$ tree timeseries
timeseries
└── settings
    └── settings.cue

settings.cue の中には先ほど時系列グラフの共通設定として定義していた #timeseriesChartSettings を切り出します。

settings.cue
package settings

#timeseriesChartSettings: {
  querySettings: [
    {
      queryIndex: int | *0
      colorMode:  string | *"fixed-single"
      colorValue: string | *"#0be300"
    },
  ]
  legend: {
    position: string | *"right"
  }
  yAxis: {
    format: {
      unit: string | "percent" | "bytes" | "percent-decimal"
    }
  }
}

そして元の cue コード内では #timeseriesChartSettings の定義を削除し、代わりに上記のローカル module から定義をインポートするように import に追加します。

package myDaC

import (
   dashboardBuilder "github.com/perses/perses/cue/dac-utils/dashboard"
   panelGroupsBuilder "github.com/perses/perses/cue/dac-utils/panelgroups"
   panelBuilder "github.com/perses/perses/cue/dac-utils/prometheus/panel"
   timeseriesChart "github.com/perses/perses/cue/schemas/panels/time-series:model"
   promQuery "github.com/perses/perses/cue/schemas/queries/prometheus:model"
+   #timeseriesChartSettings "github.com/perses-sample/timeseries/settings"
)

- #timeseriesChartSettings: {
- 	querySettings: [
- 		{
- 			queryIndex: int | *0
- 			colorMode:  string | *"fixed-single"
- 			colorValue: string | *"#0be300"
- 		},
- 	]
- 	legend: {
- 		position: string | *"right"
- 	}
- 	yAxis: {
- 		format: {
- 			unit: string | "percent" | "bytes" | "percent-decimal"
- 		}
- 	}
- }

import の記法に関しては <module identifier>/<relative position of package within module>:<package name> となっています (Import path も参照)。今回は settings.cue の package 名とディレクトリ名が同じなため、package 名を省略しても正常に import できます。

最終的なコードは以下

コード全体
ディレクトリ構造
.
├── cue.mod
│   ├── module.cue
│   ├── pkg
│   │   └── github.com
│   │       └── ...
│   └── usr
├── my_dashboard_module.cue
└── timeseries
    └── settings
        └── settings.cue
my_dashboard_module.cue
package myDaC

import (
   dashboardBuilder "github.com/perses/perses/cue/dac-utils/dashboard"
   panelGroupsBuilder "github.com/perses/perses/cue/dac-utils/panelgroups"
   panelBuilder "github.com/perses/perses/cue/dac-utils/prometheus/panel"
   timeseriesChart "github.com/perses/perses/cue/schemas/panels/time-series:model"
   promQuery "github.com/perses/perses/cue/schemas/queries/prometheus:model"
   #timeseriesChartSettings "github.com/perses/timeseries/settings"
)


#nodeStoragePanel: panelBuilder & {
   spec: {
    display: name: "Storage utilization"
    plugin: timeseriesChart & {
        spec: #timeseriesChartSettings & {yAxis: format: unit: "percent"}
    }
    queries: [
        {
               kind: "TimeSeriesQuery"
               spec: plugin: promQuery & {
                spec: {
                  query:            "(sum(node_filesystem_size_bytes{fstype!='tmpfs', fstype!='overlay'}) by (instance) - sum(node_filesystem_free_bytes{fstype!='tmpfs', fstype!='overlay'}) by (instance) ) / sum(node_filesystem_size_bytes{fstype!='tmpfs', fstype!='overlay'}) by (instance) * 100"
                  seriesNameFormat: "{{instance}}"
                }
               }
        },
    ]
   }
}

#nodeCpuUtilizationPanel: panelBuilder & {
   spec: {
    display: name: "CPU utilization"
    plugin: timeseriesChart & {
        spec: #timeseriesChartSettings & {yAxis: format: unit: "percent-decimal"}
    }
    queries: [
        {
               kind: "TimeSeriesQuery"
               spec: plugin: promQuery & {
                spec: {
                  query:            "1 - (sum(rate(node_cpu_seconds_total{mode='idle'}[1m])) by (instance) / sum(rate(node_cpu_seconds_total[1m])) by (instance))"
                  seriesNameFormat: "{{instance}}"
                }
               }
        },
    ]
   }
}

#nodeMemoryPanel: panelBuilder & {
   spec: {
    display: name: "Memory utilization"
    plugin: timeseriesChart & {
        spec: #timeseriesChartSettings & {yAxis: format: unit: "bytes"}
    }
    queries: [
        {
               kind: "TimeSeriesQuery"
               spec: plugin: promQuery & {
                spec: {
                  query:            "node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes"
                  seriesNameFormat: "{{instance}}"
                }
               }
        },
    ]
   }
}

dashboardBuilder & {
   #name:            "node-dashboard"
   #project:         "test-project"
   #duration:        "1h"
   #refreshInterval: "30s"
   #panelGroups: panelGroupsBuilder & {
    #input: [
        {
               #title:  "Node usage"
               #cols:   2
               #height: 10
               #panels: [
                #nodeCpuUtilizationPanel,
                #nodeMemoryPanel,
                #nodeStoragePanel,
               ]
        },
    ]
   }
}

ビルドはダッシュボードが定義されている my_dashboard_module.cue のみ実行すれば ok

$ percli dac build -f my_dashboard_module.cue
Succesfully built my_dashboard_module.cue at built/my_dashboard_module_output.yaml

$ percli apply -f built/my_dashboard_module_output.yaml
object "Dashboard" "node-dashboard" has been applied in the project "test-project"

上記のようにリソースを定義するファイルを分割できるので、多くのダッシュボードを作成するような状況で共通の部分を適切に分割、インポートすることでより効率的に管理できるようになります。ただ perses ドキュメント等ではファイルを分割して管理する方法や方針が記載されてなさそうなのでこれが正当なやり方かどうかは不明。

パネルのレイアウトを変更する

panelGroupsBuilder を使ってパネルを作る場合、ダッシュボード上のパネルのレイアウトは col に設定した値に基づいてグリッド状に配置されます。例えば dashboard の panelGroupsBuilder で cols: 2 に設定して 3 つのパネルを指定すると 1 行に付き 2 つまでのパネルが配置されます。3 つ目のパネルは入り切らないため、2 行目に分割されます。

dashboardBuilder & {
   #name:            "node-dashboard"
   #project:         "test-project"
   #duration:        "1h"
   #refreshInterval: "30s"
   #panelGroups: panelGroupsBuilder & {
    #input: [
        {
               #title:  "Node usage"
               #cols:   2
               #height: 10
               #panels: [
                #nodeCpuUtilizationPanel,
                #nodeMemoryPanel,
                #nodeStoragePanel,
               ]
        },
    ]
   }
}

パネルの横幅はビルド時に動的に設定されますが上記の例ではいずれも width 12 に設定されています。一方で高さ height: 10 は panelGroupsBuilder に設定した値が継承されます。

  layouts:
    - kind: Grid
      spec:
        display:
          title: Node usage
        items:
          - x: 0
            "y": 0
            width: 12
            height: 10
            content:
              $ref: '#/spec/panels/0_0'
          - x: 12
            "y": 0
            width: 12
            height: 10
            content:
              $ref: '#/spec/panels/0_1'
          - x: 0
            "y": 10
            width: 12
            height: 10
            content:
              $ref: '#/spec/panels/0_2'

これからわかるように 1 行あたりの width の合計は 24 で、col に合わせて 24 / col がパネルの width に設定されます。対応する処理は以下。

https://github.com/perses/perses/blob/8dde0e74251cb707531193f87e3b34878d2b031e/cue/dac-utils/panelgroup/panelgroup.cue#L36-L44

panel の width, height や x, y 座標は上記の panelGroupsBuilder の処理内で設定しているため各 panel をグリッド状に等間隔に配置する分には問題ないですが、パネルのサイズをパネル毎に設定したり配置を工夫したい場合は panelGroupsBuilder を使わずにこれらを設定する必要があります。

dashboardBuilder に対応する dashboard.cue を見ると、panelGroups の定義は以下のようになっています。

#panelGroups: [string]: {
  layout: v1Dashboard.#Layout
  panels: [string]: v1.#Panel
}

panelGroupsBuilder を使う場合は上記のプロパティが自動で設定されるので、逆に手動で設定すればパネルのレイアウトも手動で設定できそうだと推測されます。
v1Dashboard.#Layout は layout_patch.cue 内の #Layout で定義されています。これを読んでいくと Layout のデータ構造は以下のようになっていることがわかります。

Layout の cue データ構造
Layout: {
  kind: "Grid"
  spec: {
    {
      display: {
        title: "panel group1 name"
        collapse?: {
          open: bool
        }
      }
      items: [
        {
          x:     int
          y:      int
          height: int
          width:  int
          content: {
            $ref: "..."
        },
        ....
      ]
    }
  }
}

panels の方の v1.#Panel の定義はおそらく https://github.com/perses/perses/blob/main/cue/model/api/v1/dashboard_go_gen.cue#L26 に該当。これは panelBuilder で作成されるデータがそのまま使用できます。

以上をまとめると、panelGroupsBuilder を使用しない場合には panelGroups に以下のようなデータ構造を指定することでダッシュボードが作成できます。試しに 2 つのパネルを配置し、個々のパネルのサイズや座標を変更したダッシュボードを作ってみます。

#panelGroups: {
    "group1": {
        layout: #Layout
        panels: {
            "panelA": #panelBuilder
            "panelB": #panelBuilder
            ...
        }
    }
    "group2": {
        layout: #Layout
        panels: {
            "panelA": #panelBuilder
            "panelB": #panelBuilder
            ...
        }
    }
    ...
}

一方で Layout の中では自分でパネルの x, y やサイズを指定できるので、これにより個々のパネルの配置等を手動で調整できます。

コード全文
package myDaC

import (
  dashboardBuilder "github.com/perses/perses/cue/dac-utils/dashboard"
  panelBuilder "github.com/perses/perses/cue/dac-utils/prometheus/panel"
  timeseriesChart "github.com/perses/perses/cue/schemas/panels/time-series:model"
  promQuery "github.com/perses/perses/cue/schemas/queries/prometheus:model"
  #timeseriesChartSettings "github.com/perses-sample/timeseries/settings"
)

#nodeCpuUtilizationPanel: panelBuilder & {
  spec: {
    display: name: "CPU utilization"
    plugin: timeseriesChart & {
      spec: #timeseriesChartSettings & {yAxis: format: unit: "percent-decimal"}
    }
    queries: [
      {
        kind: "TimeSeriesQuery"
        spec: plugin: promQuery & {
          spec: {
            query:            "1 - (sum(rate(node_cpu_seconds_total{mode='idle'}[1m])) by (instance) / sum(rate(node_cpu_seconds_total[1m])) by (instance))"
            seriesNameFormat: "{{instance}}"
          }
        }
      },
    ]
  }
}

#nodeMemoryPanel: panelBuilder & {
  spec: {
    display: name: "Memory utilization"
    plugin: timeseriesChart & {
      spec: #timeseriesChartSettings & {yAxis: format: unit: "bytes"}
    }
    queries: [
      {
        kind: "TimeSeriesQuery"
        spec: plugin: promQuery & {
          spec: {
            query:            "node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes"
            seriesNameFormat: "{{instance}}"
          }
        }
      },
    ]
  }
}

#myLayout: {
  kind: "Grid"
  spec: {
    {
      display: {
        title: "panel group1 name"
      }
      items: [
        {
          x:      0
          y:      10
          height: 10
          width:  5
          content: $ref: "#/spec/panels/cpuUtil"
        },
        {
          x:      8
          y:      10
          height: 6
          width:  12
          content: $ref: "#/spec/panels/memoryUtil"
        },
      ]
    }
  }
}

dashboardBuilder & {
  #name:            "custom-dashboard"
  #project:         "test-project"
  #duration:        "1h"
  #refreshInterval: "30s"
  #panelGroups: {
    "group1": {
      layout: #myLayout
      panels: {
        "cpuUtil":    #nodeCpuUtilizationPanel
        "memoryUtil": #nodeMemoryPanel
      }
    }
  }
}

ビルドで生成される yaml を見ると Grid の座標が想定通りに設定されていることがわかります。また、0_0 など自動で設定されていた panel のインデックスも cpuUtil など指定した値に設定されています。

kind: Dashboard
metadata:
  name: custom-dashboard
  createdAt: "0001-01-01T00:00:00Z"
  updatedAt: "0001-01-01T00:00:00Z"
  version: 0
  project: test-project
spec:
  panels:
    cpuUtil:
      kind: Panel
      spec:
        display:
          name: CPU utilization
        plugin:
          kind: TimeSeriesChart
          spec:
            yAxis:
              format:
                unit: percent-decimal
        queries:
          - kind: TimeSeriesQuery
            spec:
              plugin:
                kind: PrometheusTimeSeriesQuery
                spec:
                  query: 1 - (sum(rate(node_cpu_seconds_total{mode='idle'}[1m])) by (instance) / sum(rate(node_cpu_seconds_total[1m])) by (instance))
                  seriesNameFormat: '{{instance}}'
    memoryUtil:
      kind: Panel
      spec:
        display:
          name: Memory utilization
        plugin:
          kind: TimeSeriesChart
          spec:
            yAxis:
              format:
                unit: bytes
        queries:
          - kind: TimeSeriesQuery
            spec:
              plugin:
                kind: PrometheusTimeSeriesQuery
                spec:
                  query: node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes
                  seriesNameFormat: '{{instance}}'
  layouts:
    - kind: Grid
      spec:
        display:
          title: panel group1 name
        items:
          - x: 0
            "y": 10
            height: 10
            width: 5
            content:
              $ref: '#/spec/panels/cpuUtil'
          - x: 8
            "y": 10
            height: 6
            width: 12
            content:
              $ref: '#/spec/panels/memoryUtil'
  duration: 1h
  refreshInterval: 30s

デプロイすると想定通りパネル毎にサイズが変更されていたり、パネル間に空白が設定されています。


パネル毎の高さや幅、パネルの間隔を調整できる

panelGroupsBuilder を使う場合と比較すると記述はやや煩雑になりますが、上記の方法で個々のパネルの配置やサイズも制御することができます。

その他

CI/CD

現時点では percli dac build でコードから yaml を生成 → percli apply でデプロイという手順で DaC を実現するため、CI/CD に組み込むには CI/CD の処理の中で上記コマンドを実行する部分を追加することになります。ドキュメントでは CI/CD setup のセクションがありますがまだ TODO なので、将来的には CI/CD に統合するような処理や仕組みが追加されるかもしれません。

データの永続化

今回の検証ではデータは perses 内部に保存されているため、perses コンテナが削除されるとデータも失われます。データを永続化させるには perses の config で database に接続先のデータベースの情報を記載します。

config-db.yml
security:
  readonly: false
  enable_auth: true
  cookie:
    same_site: lax
    secure: false
  authentication:
    access_token_ttl: 24h
    refresh_token_ttl: 24h
    providers:
      enable_native: true
  authorization:
    guest_permissions:
      - actions: ["*"]
        scopes: ["*"]
database:
  sql:
    user: perses
    password: perses
    db_name: perses
    net: tcp
    addr: mariadb:3306
schemas:
  datasources_path: /etc/perses/cue/schemas/datasources
  interval: 5m
  panels_path: /etc/perses/cue/schemas/panels
  queries_path: /etc/perses/cue/schemas/queries
  variables_path: /etc/perses/cue/schemas/variables

docker compose では接続先の mysql コンテナを追加。

docker-compose.yml
services:
  perses:
    container_name: perses
    image: persesdev/perses:v0.48.0
    ports:
      - 8080:8080
    command:
        - --config=/etc/perses/config/config.yml
        - --web.listen-address=:8080
        - --web.hide-port=false
        - --web.telemetry-path=/metrics
        - --log.level=info
        - --log.method-trace=true
    volumes:
      - type: bind
        source: ./config-db.yml
        target: /etc/perses/config/config.yml
      - ./secret:/etc/perses/config
    depends_on:
      mysql:
        condition: service_healthy
  mysql:
    container_name: mysql
    image: mysql
    ports:
      - 3306:3306
    volumes:
      - ./data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: admin
      MYSQL_DATABASE: perses
      MYSQL_USER: perses
      MYSQL_PASSWORD: perses
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-padmin"]
      interval: 10s
      timeout: 2s
      retries:
      start_period: 15s

これでデータが mysql に保存されて永続化されるようになります。
サポートされているデータベースの種類は明記されていないようですが、上記の通り mysql は使用できます(postgres を指定した場合はエラーになった)。

おわりに

perses を使って prometheus メトリクス用のダッシュボードを DaC で作成する方法をいくつか試しました。リソースの概念やダッシュボードで設定可能な項目は Grafana を踏襲しているため、Grafana を使い慣れているユーザーはそれほど苦戦せずに使い始めることができます。
DaC のためのコードは cue か go を使って書く必要がありますが、cue はけっこう表記の癖があるのではじめて触る場合にはやや学習コストが高いのが難点です。go に慣れていれば go SDK でも記述できるので問題ないかもしれませんが。
また、perses は sandbox プロジェクトで発展段階ということもありドキュメントの記述や情報がやや不足気味に感じました。記述が不明瞭な部分は実際にコードを書いて試したり github 上で該当する情報がないか検索する必要があります。とはいえ PromCon でも何度か取り上げられているプロジェクトであり、以前に紹介した Thanos と合わせて Prometheus ecosystem の一部として注目を集めています。Prometheus 3.0 も最近リリースされたばかりなので、Prometheus-native なダッシュボードの DaC システムとして今後の発展が期待されます。

Discussion