※当サイトの記事には、広告・プロモーションが含まれます。

grafana.iniの設定項目とGrafanaの環境変数の対応の一覧が欲しいのでPythonで対応表を作成してみた

gigazine.net

Mesh LLMは、複数のコンピューターの余剰GPU計算資源を持ち寄り、1台では動かしにくい大規模言語モデルを分散実行できるようにする仕組みです。OpenAI互換APIを備えており、既存のAIツールやエージェントからそのまま使いやすい点が特徴です。

複数のPCからリソースをかき集めて巨大なAIモデルをローカル実行できる「mesh-llm」 - GIGAZINE

Mesh LLMは余剰GPUを束ねて大きなモデルを動かすための分散推論基盤であり、同時に複数モデル運用やエージェント連携、知識共有まで含めて扱えるのが特徴です。

複数のPCからリソースをかき集めて巨大なAIモデルをローカル実行できる「mesh-llm」 - GIGAZINE

また、自分専用のプライベートメッシュだけでなく、他人と共有できる公開メッシュを作成することもできます。1台では重すぎるLLMをみんなの計算資源で動かせるようにしつつ、使い勝手もなるべく普通のAIサービスに近づけた仕組みだといえます。

複数のPCからリソースをかき集めて巨大なAIモデルをローカル実行できる「mesh-llm」 - GIGAZINE

仕組みとしては、モデルが1台のマシンに収まる場合はそのままローカルで実行し、収まらない場合だけ複数ノードに分散します。通常のDense modelではレイヤーごとに処理を分けるパイプライン並列化を使い、MoEモデルではエキスパートを各ノードに分散配置する方式を取ります。特にMoEでは、推論中のノード間通信をできるだけ減らすよう設計されています。

複数のPCからリソースをかき集めて巨大なAIモデルをローカル実行できる「mesh-llm」 - GIGAZINE

⇧ とりあえず、「Kubernetes」のような「オーケストレーション」的な「仕組み」を実現できるということなのかね?

github.com

⇧ 上記を見た限り、「ノード」の追加の方法が「Kubernetes」の構築時みたいな感じなので...

まぁ、いずれにしろ、「AI」の「モデル」を動作させる必要があるので、

  1. 高スペックな単一マシン
  2. 高スペックではないが複数のマシン

のいずれかの「環境」が必要になって来るとは思うのだが、「事前要件(prerequire)」を「README.md」に記載しておいて欲しいよね...

最低限、どれぐらいの「スペック」の「マシン」を用意する必要があるのかはハッキリさせて欲しいよね...

grafana.iniの設定項目とGrafanaの環境変数の対応の一覧が欲しいのでPythonで対応表を作成してみた

公式の「ドキュメント」によると、

grafana.com

⇧ 上記にありますように、「<WORKING DIRECTORY>/conf/defaults.ini」というファイルに、「Grafana」の「デフォルト」の「設定」についての「情報」が集約されているらしい。

で、

grafana.com

⇧ 上記にありますように、「Grafana」用の「環境変数」が用意されており、「grafana.ini」のような「Grafana」用の「設定」ファイルの内容を上書きできる仕組みが用意されている。

「Grafana」用の「環境変数」は、

GF_<SECTION_NAME>_<KEY>

⇧ 上記のような「フォーマット」になっており、

Where <SECTION NAME> is the text within the square brackets ([ and ]) in the configuration file. All letters must be uppercase, periods (.) and dashes (-) must replaced by underscores (_).

https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#configuration-file-location

⇧ 上記のルールにあるように、「Grafana」用の「設定」が記載された「ini」ファイル内の「情報」を元にしている。

であるからして、「Grafana」用の「環境変数」を見ただけでは、

  1. SECTION_NAME
  2. KEY

の境界が分からないのよね、いずれも、「アンダースコア(_)」で区切られているわけなので...

「Grafana」用の「環境変数」の「フォーマット」が、「Grafana」側の都合で、「Grafana」用の「設定」が記載された「ini」ファイル内の「情報」である「SECTION_NAME」の値である

  1. ピリオド(.
  2. ハイフン(-

が勝手に「アンダースコア(_)」に変換されてしまっており、且つ、

  1. SECTION_NAME
  2. KEY

は、「アンダースコア(_)」によって連結されてしまっていることもあり、どうしようもないのよね...

 

ちなみに、

grafana.com

⇧ 上記にありますように、「grafana.ini」のような「Grafana」用の「設定」ファイル内で「変数」を利用できる仕組みも用意されている。

ただ、「Grafana」用の「ini」ファイル側に「置き換え前」の「expression」を書き込んでおく必要がありますと。

対応している「変数」の「方式」としては、3種類になる模様。

No 対応している変数の方式 内容
1 env provider The env provider expands environment variables.
2 file provider The file provider reads a file from the filesystem
3 vault provider The vault provider lets manage your secrets with Hashicorp Vault.

⇧ 馴染みのあるのは、「env provider」の「環境変数」ですかね。

ただ、「.env」ファイルなどは、「.gitignore」に含められていて「Git」管理されていないことあるあるなので、「Grafana」用の「環境変数」の具体的な値が実際の環境を覗いてみないと分からないとかあって辛いのよね...

どう管理するのが良いのかしらね...

 

で、表題の件。

一応、

github.com

⇧ 上記で「defaults.ini」というファイルは公開されているので、「ChatGPT」氏に、

  1. Grafana用の設定ファイルの設定項目
  2. Grafana用の環境変数

の対応の一覧を生成する「Python」の「ソースコード」をお願いしてみた。

まぁ、

  1. defaults.ini」ファイル
  2. grafana.ini」ファイル

の「設定項目」の数が変わらないと仮定が正しいとした場合に、「Grafana」用の「設定」の全量が把握できるのかもしれないが、「defaults.ini」ファイルに「Grafana」用の「設定」の全量が集約されていない場合は、他の「Grafana」用の「設定」ファイルを確認する必要がありそうではある...

 

grafana_env_mapper/
├─ requirements.txt
└─ src/
    └─ grafana_mapper/
        │
        ├─ domain/
        │   └─ models.py
        │
        ├─ usecase/
        │   └─ parse_ini_usecase.py
        │
        ├─ interface/
        │   └─ writer_interface.py
        │
        ├─ infrastructure/
        │   ├─ downloader.py
        │   ├─ git_meta_fetcher.py
        │   └─ csv_writer.py
        │
        └─ main.py

<p⇧ のようなディレクトリ構成。 <p■D:\work-soft\python\grafana_env_mapper\requirements.txt

pydantic>=2.3
pandas>=2.1
requests>=2.31
pytest>=7.4    

<p⇧ <p■D:\work-soft\python\grafana_env_mapper\src\domain\models.py

from pydantic import BaseModel, Field

class IniMapping(BaseModel):
    no: int = Field(..., description="行番号")
    section: str = Field(..., description="INIセクション名")
    key: str = Field(..., description="INIキー")
    default: str = Field(..., description="デフォルト値")
    env: str = Field(..., description="対応する環境変数名")
    description: str = Field("", description="INIコメント / 説明文")

    class Config:
        str_strip_whitespace = True
        str_min_length = 0    

■D:\work-soft\python\grafana_env_mapper\src\interface\writer_interface.py

from typing import List
from abc import ABC, abstractmethod
from src.domain.models import IniMapping

class WriterInterface(ABC):
    @abstractmethod
    def write(self, rows: List[IniMapping], meta: dict = None, version: str = None) -&> None:
        """INIマッピングを外部に書き出す"""
        pass    

■D:\work-soft\python\grafana_env_mapper\src\infrastructure\csv_writer.py

import pandas as pd
from typing import List, Optional, Dict
from src.domain.models import IniMapping
from src.interface.writer_interface import WriterInterface

class CSVWriter(WriterInterface):
    def __init__(self, output_file: str = "grafana_env_mapping.csv") -> None:
        self.output_file: str = output_file

    def write(
        self,
        rows: List[IniMapping],
        meta: Optional[Dict[str, str]] = None,
        version: Optional[str] = None
    ) -> None:
        """
        CSV出力。
        - rows: IniMappingのリスト
        - meta: GitHub commit情報 {"sha": str, "date": str} など
        - version: Grafanaバージョン
        """
        # Pydantic v2: model_dump() で dict に変換
        data: List[Dict[str, str]] = [r.model_dump() for r in rows]
        df: pd.DataFrame = pd.DataFrame(data)

        # 先頭にコメント行を作成
        comment_lines: List[str] = []
        if meta is not None and version is not None:
            comment_lines.append(f"# Grafana Version: {version}")
            sha: str = meta.get("sha", "")
            date: str = meta.get("date", "")
            comment_lines.append(f"# Commit: {sha}, Date: {date}")

        # CSV出力
        with open(self.output_file, "w", encoding="utf-8", newline="") as f:
            # コメント行書き込み
            for line in comment_lines:
                f.write(line + "\n")
            # データ行書き込み
            df.to_csv(f, index=False)

        print(f"CSV出力完了: {self.output_file}")

■D:\work-soft\python\grafana_env_mapper\src\infrastructure\downloader.py

import requests

class Downloader:
    def __init__(self, repo: str, ref: str):
        self.repo = repo
        self.ref = ref

    def download_file(self, path: str, output_name: str) -> None:
        url = f"https://raw.githubusercontent.com/{self.repo}/{self.ref}/{path}"
        res = requests.get(url)
        res.raise_for_status()
        with open(output_name, "w", encoding="utf-8") as f:
            f.write(res.text)    

■D:\work-soft\python\grafana_env_mapper\src\infrastructure\git_meta_fetcher.py

import requests

class GitMetaFetcher:
    def __init__(self, repo: str, ref: str):
        self.repo = repo
        self.ref = ref

    def fetch_commit_info(self) -> dict:
        url = f"https://api.github.com/repos/{self.repo}/commits/{self.ref}"
        res = requests.get(url)
        res.raise_for_status()
        data = res.json()
        return {
            "sha": data["sha"],
            "date": data["commit"]["author"]["date"],
            "url": data["html_url"],
            "message": data["commit"]["message"]
        }    

■D:\work-soft\python\grafana_env_mapper\src\usecase\parse_ini_usecase.py

from typing import List
from src.domain.models import IniMapping

class ParseIniUsecase:
    def __init__(self, file_path: str = "defaults.ini"):
        self.file_path = file_path

    def execute(self) -> List[IniMapping]:
        mappings: List[IniMapping] = []
        current_section: str | None = None
        comment_buffer: list[str] = []
        no = 1

        with open(self.file_path, encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if not line:
                    comment_buffer = []
                    continue
                if line.startswith(";") or line.startswith("#"):
                    comment_buffer.append(line.lstrip(";# ").strip())
                    continue
                if line.startswith("["):
                    current_section = line.strip("[]")
                    comment_buffer = []
                    continue
                if "=" in line and current_section:
                    key, default = map(str.strip, line.split("=", 1))
                    mapping = IniMapping(
                        no=no,
                        section=current_section,
                        key=key,
                        default=default,
                        env=f"GF_{current_section.replace('.', '_').upper()}_{key.replace('.', '_').upper()}",
                        description=" ".join(comment_buffer)
                    )
                    mappings.append(mapping)
                    no += 1
                    comment_buffer = []

        return mappings    
import pandas as pd

def generate_ini_section_summary(input_csv: str, output_csv: str) -> None:
    """
    出力CSVからSECTIONごとのKEY数を集計して別CSVに出力
    コメント行 (#) は無視して読み込む
    """
    # コメント行を無視して読み込む
    df: pd.DataFrame = pd.read_csv(input_csv, comment="#")

    # SECTIONごとにKEY数を集計
    section_counts: pd.DataFrame = df.groupby("section")["key"].count().reset_index()
    section_counts = section_counts.rename(columns={"key": "key_count"})

    # 先頭列にNoを追加
    section_counts.insert(0, "No", range(1, len(section_counts) + 1))

    # 集計結果をCSV出力
    section_counts.to_csv(output_csv, index=False)
    print(f"SECTION集計CSV出力完了: {output_csv}")    

■D:\work-soft\python\grafana_env_mapper\src\main.py

from datetime import datetime
from typing import Dict, List

from src.usecase.parse_ini_usecase import ParseIniUsecase
from src.infrastructure.downloader import Downloader
from src.infrastructure.git_meta_fetcher import GitMetaFetcher
from src.infrastructure.csv_writer import CSVWriter
from src.infrastructure.ini_section_summary import generate_ini_section_summary
from src.domain.models import IniMapping


def main() -> None:
    repo: str = "grafana/grafana"
    ref: str = "v12.4.2"

    # 現在日時を取得してタイムスタンプ生成
    now: datetime = datetime.now()
    timestamp: str = now.strftime("%Y%m%d-%H%M%S")

    # ファイル名作成
    output_file: str = f"grafana_env_mapping_{ref}_{timestamp}.csv"
    summary_output_file: str = f"section_summary_{ref}_{timestamp}.csv"

    # defaults.ini をダウンロード
    downloader: Downloader = Downloader(repo, ref)
    downloader.download_file("conf/defaults.ini", "defaults.ini")

    # GitHubコミット情報取得
    commit_meta: Dict[str, str] = GitMetaFetcher(repo, ref).fetch_commit_info()

    # commit_meta のキー追加も型注釈
    commit_meta["repo"] = repo
    commit_meta["ref"] = ref

    # INI解析 → マッピング生成
    mappings: List[IniMapping] = ParseIniUsecase("defaults.ini").execute()

    # 元CSV出力(先頭コメント行付き)
    csv_writer: CSVWriter = CSVWriter(output_file)
    csv_writer.write(mappings, commit_meta, ref)

    # SECTION集計CSV出力(コメント行なし)
    generate_ini_section_summary(output_file, summary_output_file)

    print(f"✅ 完了: Grafana Version {ref}, {len(mappings)} 行のマッピング生成")
    print(f"元CSV: {output_file}")
    print(f"SECTION集計CSV: {summary_output_file}")


if __name__ == "__main__":
    main()

⇧ 上記のファイルを用意し、「venv」で「Python仮想環境」は作成し、「Python仮想環境」に切り替えて、「requirements.txt」に記載のライブラリをインストールしてから、「main.py」を実行する。

 

「はてなブログ」の行数の制約上、出力された「CSVファイル」の結果は割愛。

 

毎度モヤモヤ感が半端ない…

今回はこのへんで。