kubesense-sql について
kubesense-sqlは、KubeSense SQLツールでログやトレースを検索するためのスキルです。結合やウィンドウ関数など複雑な結果形式に対応し、フィールド確認、SQL検証、実行と、インデックスを使ったログ検索や制限を案内します。
主な機能
- 01ログとトレースのフィールド確認
- 02実行前の読み取り専用SQL検証
- 03SQLを実行してTSV行を取得
- 04時刻とクラスターのマクロ利用
- 05インデックス付きログ検索の案内
活用シーン
- 01ログまたはトレース結果の結合
- 02単一クエリでの期間比較
- 03CTEやウィンドウ関数による導出
- 04インデックス付きテキスト条件でのログ検索
MCP 対応
KubeSense MCPサーバーが必要です。ログやトレースのフィールド確認、クエリ検証、SQL実行に使うSQLツールを説明します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ kubesense-ai/ kubesense-mcp-skills --skill kubesense-sqlインストール後、Claude Code で/kubesense-sqlを入力して読み込まれたか確認します。
同梱ファイルreferences/recipes.md
詳細
KubeSense SQL
KubeSense MCP サーバーが必要です。接続と認証については、kubesense-mcp を参照してください。ここにあるツールは、指定した signal に応じて、logs モジュールまたは traces モジュールに対して RBAC を解決します。
ツール
| ツール | 使用するタイミング | 戻り値 |
|---|---|---|
get-trace-or-log-fields | 常に最初に使用 | 指定した時間範囲のフィールドカタログ |
validate-sql | 複雑なクエリを実行する前 | valid=true/false + 実行される SQL |
execute-sql | クエリが適切な場合 | TSV 形式の結果行 |
どちらの SQL ツールも、同じ引数を取ります。signal("logs" または "traces")、query、from_time、to_time、および任意の clusters です。
analyze-* で対応できない場合にのみ SQL を使用する
analyze-logs / analyze-traces で、フィルター → グループ化 → 集計を実行できます。ほとんどの質問はこれで対応できます。こちらのほうが低コストで、引数のチェックもより厳格であり、出力も小さくなります。結果の形がどうしても合わない場合に限り、SQL を使用してください。
| 質問の形 | ツール |
|---|---|
| グループ化したカウント / レート / p95 / 上位N件 | analyze-logs、analyze-traces |
| 2つのメトリクスを割る(エラー率%) | 数式を指定した analyze-telemetry |
| 個々の行を読み取る | search-logs、search-traces |
| 2つの結果セットを結合する | SQL |
| CTE の連鎖 / 複数段階の導出 | SQL |
ウィンドウ関数(row_number、lag、累積合計) | SQL |
| 1回の処理で2つの集計値間の演算を行う | SQL |
UNION、DISTINCT ON、argMax、quantileIf | SQL |
| 1つの結果で2つの時間範囲を比較する | SQL |
SELECT field, count(*) ... GROUP BY field を書き始めたら、そこで止めてください。これは value_operation: row_count を指定した analyze-* で実行できます。
フィルター条件は、SQL を使う理由にはなりません
search-* / analyze-* の where 引数は、Logs ページと Traces ページの Advanced クエリタブと同じ言語です。条件をそこに記述できるなら、そこで記述してください。サーバーは body 検索をテキストインデックス形式に書き換え、列だけを使うフィルターを事前集計済みのロールアップテーブルにルーティングします。SQL ではどちらも行われません。SQL は常に未加工のテーブルを読み取り、入力されたとおりの述語を実行します。
次の順に判断してください。
- 条件を
where文字列で記述できますか。=!=<>LIKEILIKESUBSTR_ILIKEIN、AND/OR/NOT、@attrフィルター、body ILIKE "%…%"などが使えます。→search-*/analyze-*を使用してください。これで、「…に該当する行を検索 / 件数を数える」というケースのほとんどに対応できます。 - 結果の形だけが合わないのでしょうか(結合、CTE、ウィンドウ関数、集計値間の演算など)。→ SQL を使用し、同じ条件を使ってください。
bodyに関するものには、以下の ログテキストの検索 の手順に従ってください。 - 条件自体に、
where言語では使えないもの(正規表現、2つの列間の比較、列に対する関数)が必要ですか。→ SQL を使用し、まずスキャン範囲を絞るため、インデックス付きの述語も併用してください(workload/namespaceの等価条件や、正規表現に含まれる必要がある語に対するhasToken(lower(body), …)など)。
1つのフィールドに複数の値を指定する場合は、where 文字列で IN または OR を使用してください。それを理由に SQL を使う必要はありません。
テーブル
| 記述 | 読み取り対象 |
|---|---|
FROM logs | logs テーブル |
FROM traces | traces テーブル |
これ以外は参照できません。データベース名を含む名前(kube_logs.logs_combined)は即座に拒否されます。これは、別の SQL ツールからクエリを貼り付けたときに最初に問題となる箇所です。signal は FROM で選択するテーブルと一致している必要があります。
フィールド名
他のすべての KubeSense ツールと同じカタログラベル — get-trace-or-log-fields が返す項目です。ストレージの列名は拒否されます。
| 書く名前 | 書かない名前 |
|---|---|
type | level |
instance | pod_name |
container | container_name |
domain | cluster(下記参照)、customer_identifier |
service | app_service |
role | kind |
status_code | return_code |
method | subtype |
resource | clustered_resource |
reason | issue_reason |
request_type | is_external |
timestamp | start_timestamp |
duration_ms | duration |
SELECT * は、生テーブルではなくカタログの列に展開されます。
SQL で使えるが、ディスカバリーには表示されない4つの項目
get-trace-or-log-fields は、フィルター不可としてフラグが設定されたフィールドを表示しませんが、SQL では引き続き使用できます。
ディスカバリーに表示されないからといって、無効な名前だと判断しないでください。
| フィールド | 対象 | 備考 |
|---|---|---|
timestamp | 両方 | ディスカバリーには表示されませんが、すべてのクエリで $__timeFilter(timestamp) に必要です |
duration_ms | トレース | ミリ秒単位です。ディスカバリーには duration(ナノ秒)と表示されますが、SQL ではその名前は拒否されます |
primary_namespace, primary_workload | トレース | 視点を示す列です |
body_length | ログ | length(body) として計算されます |
カタログフィールドではありませんが、クエリ可能な項目もあります。cluster(domain と同じ列で、$__clusters が展開される先です)と、生の属性コンテナー(ログでは string_attributes/float_attributes、トレースでは attribute_names/attribute_values)です。
警告
最も注意が必要なのは
durationとduration_msの違いです。analyze-tracesではナノ秒単位のdurationを指定します。SQL ではミリ秒単位のduration_msを指定し、durationは一切受け付けません。analyzeの呼び出しからしきい値を引き継ぐと、10⁶ 倍ずれてしまいます。
マクロ
クエリの実行前に、サーバー側で展開されます。
| マクロ | 展開先 |
|---|---|
$__timeFilter(timestamp) | from_time/to_time の期間 |
$__fromTime/$__toTime | 式で使うための、境界を表すスカラー値 |
$__clusters | クラスターのフィルター。clusters が空の場合は 1=1 |
SELECT workload, count(*) AS cnt
FROM logs
WHERE $__timeFilter(timestamp) AND $__clusters AND type = 'ERROR'
GROUP BY workload ORDER BY cnt DESC
$__timeFilter は、両方のシグナルで列名 timestamp を受け取ります。通常の SQL 時刻述語も使えます(WHERE timestamp >= now() - INTERVAL 1 HOUR)。ただし、マクロを使うとクエリを指定した期間に合わせられます。
メモ
SQL では文字列値にシングルクォート(
type = 'ERROR')を使います。これは、分析/検索ツールのwhere引数で、引用符なしの単語またはダブルクォートを使うのとは異なります。列挙値の大文字・小文字は変わりません。ログではERROR(大文字)、トレースのstatusではerror(小文字)です。
カスタム属性
他のすべての箇所と同様に、先頭に @ を付けます。@http.status_code、または @"key.with spaces" のように指定します。
SELECT @http.status_code, count(*) AS cnt
FROM logs
WHERE $__timeFilter(timestamp) AND @http.status_code != ''
GROUP BY 1 ORDER BY cnt DESC
属性は文字列として保存されます。数値として比較する前にキャストしてください。
WHERE toFloat64OrNull(@latency_ms) > 500
警告
属性名は、どのツールでも、どの段階でも検証されません。
@式はクエリが解析される前にマップ検索へコンパイルされるため、キーのスペルを間違えてもエラーにはなりません。ログではNULL、トレースでは''が返されます。validate-sqlはvalid=trueを報告します。ある列の値がすべて空になるクエリは、ほとんどの場合、データが欠けているのではなく属性キーが間違っています。まずget-trace-or-log-fieldsで各キーを確認してください。
ログテキスト(body)の検索
注意
SQL で
bodyをそのまま条件に指定すると、対象ウィンドウ内のすべてのログ行がスキャンされます。bodyのインデックスはlower(body)を対象に構築されています。body LIKE、body ILIKE、position(body, …)、match(body, …)、およびposition(lower(body), …)はどのインデックスにも一致しないため、クエリはメッセージ列全体を展開します。本番クラスターでこの方法を使って数日分を検索すると、テラバイト単位のデータを読み込み、全ユーザーの取り込み処理を停滞させることがあります。
search-logs と analyze-logs は、body ILIKE "…" をインデックス付きの形式に書き換えます。SQL では書き換えられません。記述したとおりに条件が実行されます。そのため、「X を含む行を検索する」にはこれらのツールを使い、SQL 内で検索する必要がある場合は、次のいずれかを記述してください。
| 検索対象 | 記述する内容 | インデックス |
|---|---|---|
| 1 つの単語または ID | hasToken(lower(body), 'abc123') | テキストインデックス — 最適 |
| 複数候補のいずれか | hasAnyTokens(lower(body), ['abc123', 'def456']) | テキストインデックス |
| 複数候補すべて | hasAllTokens(lower(body), ['connection', 'refused']) | テキストインデックス |
| 句読点を含む完全一致の部分文字列 | lower(body) LIKE '%connection refused%' | n-gram フィルター |
- 検索語は小文字にしてください。 インデックス内のテキストは小文字化されています。
hasToken(lower(body), 'OOM')は何も一致しません。 - トークンは、英字と数字が連続した文字列です。
hasTokenが受け取れるのは 1 つだけです。'node-agent'や'a b'はエラーになります。句読点で分割してhasAllTokensを使うか、lower(body) LIKEに切り替えてください。 hasAllTokensは「これらの単語をすべて含む」という意味であり、「このフレーズを含む」という意味ではありません。単語の並び順も一致させる必要がある場合に限り、lower(body) LIKE '%…%'も追加してください。- トークン全体が一致する必要があります。
hasToken(lower(body), 'timeo')はtimeoutに一致しません。単語の一部を検索する場合は、lower(body) LIKE '%timeo%'を使ってください。
SELECT toDate(timestamp) AS d, workload, count(*) AS n
FROM logs
WHERE $__timeFilter(timestamp) AND $__clusters
AND hasAnyTokens(lower(body), ['6abffe8am1sw6', '636ce0fe6khta'])
GROUP BY d, workload ORDER BY d, n DESC
本番クラスターの 1 時間分で測定(時間による絞り込み後、9,640 個のグラニュール)したところ、body LIKE '%…%' は 9,640 個すべてを読み込み、lower(body) LIKE は 213 個、hasToken は 202 個を読み込みました。
対象を絞ってから広げてください。 分かっている場合は workload / namespace / type を追加し、数日分ではなく 1 時間から始めてください。body の検索は一度に 1 回 実行してください。同じ検索を時間帯ごとに分けて並列実行しないでください。それぞれが同じディスクを取り込み処理と奪い合います。validate-sql では警告できません。インデックスを使わない body のスキャンも、SQL としては完全に有効だからです。
validate-sql
CTE、結合、またはこれまで使ったことのないフィールドを含むクエリでは、execute-sql の前に呼び出してください。データをスキャンしないため、繰り返し呼び出してもコストは低く抑えられます。
次の 2 つを検査します。
- 書き換えの検証 — 構文解析、単一ステートメントであること、
SELECTのみであること、テーブルアクセス、ブロック対象の関数、列名を確認します。列名の確認対象は、トップレベルの SELECT リストにある裸の列参照のみです。また、クエリにWITH句がある場合は、列名の確認を完全にスキップします。 - 書き換え後のクエリに対するClickHouse 独自の解析。すべての識別子を解決します。これにより、
WHERE、GROUP BY、または CTE 内にある不正な列を検出できます。
成功すると、実際に実行される SQL が ## rewritten_sql の下に返されます。必ず確認してください。 各ラベルがどのストレージ列に解決されたか(level AS type、pod_name AS instance)が示されます。これが、意図したフィールドを参照していることを確認する唯一の方法であり、名前がいつの間にか別のフィールドに解決されていた場合に、それをすばやく見つける方法です。
出力の読み方
execute-sql は TSV を返します。# signal=… rows=N の行に続き、ユーザーが指定した SELECT 列が指定した順序で並ぶヘッダー行、さらに結果ごとに 1 行が返されます。
# signal=logs rows=2
workload cnt
checkout 412
payments 87
結果が空の場合は、ヘッダー行だけが返されます。これはクエリが実行され、何も一致しなかったことを意味します。失敗ではありません。また通常、クエリを書き換えるのは適切な対応ではありません。まず対象の時間帯とクラスターのフィルターを確認してください。
制限事項
- クエリに独自の
LIMITがない場合は、1000 行までです。行数を数えるために結果を取得するのではなく、SQL で集計してください。 - 実行は読み取り専用です。
INSERT/ALTER/DROP、複数のステートメント、およびテーブル外にアクセスする関数(sleep、url、s3、file、remote、mysql)は拒否されます。 - クラスターのフィルターはテーブルスキャン時に適用され、クエリで回避することはできません。
警告
スコープ制限付きのロールでは、どちらのツールも利用できません。 API キーまたはロールが特定のクラスター、名前空間、ワークロードに制限されている場合、呼び出しは拒否されます。SQL レイヤーではクラスターのフィルターのみが適用され、名前空間やワークロードのルールは適用できません。適用できる
search-*/analyze-*を使用してください。拒否メッセージにその旨が記載されています。再試行したり、SQL でスコープを指定しようとしたりしないでください。
使用例
この 1 時間と昨日の同じ 1 時間を、1 つの結果で比較する
SELECT service,
countIf(timestamp >= $__toTime - INTERVAL 1 HOUR) AS now_errors,
countIf(timestamp < $__fromTime + INTERVAL 1 HOUR) AS then_errors
FROM traces
WHERE $__timeFilter(timestamp) AND $__clusters AND status = 'error'
GROUP BY service HAVING now_errors > then_errors ORDER BY now_errors DESC
サービスごとに最も遅いエンドポイントを取得する(ウィンドウ関数)
SELECT service, resource, p95_ms FROM (
SELECT service, resource, quantile(0.95)(duration_ms) AS p95_ms,
row_number() OVER (PARTITION BY service ORDER BY quantile(0.95)(duration_ms) DESC) AS rn
FROM traces
WHERE $__timeFilter(timestamp) AND $__clusters AND role = 'server'
GROUP BY service, resource
) WHERE rn = 1 ORDER BY p95_ms DESC
エラーをログに記録したものの、失敗したスパンを提供していないワークロード(CTE の結合)
WITH bad_logs AS (
SELECT DISTINCT workload FROM logs
WHERE $__timeFilter(timestamp) AND $__clusters AND type = 'ERROR'
)
SELECT workload FROM bad_logs
次に 2 つ目の traces クエリを実行します。2 つのシグナルは別々のテーブルにあり、1 つのステートメントでは結合できません。結果は自分で関連付けるか、1 回の呼び出しでシグナルを組み合わせる analyze-telemetry を使用してください。
その他のパターン:references/recipes.md。
ルール
- まず
analyze-*を試してください。SQL は、結合、CTE、ウィンドウ関数、または集計間の算術演算にのみ使用してください。条件をwhere文字列(Advanced query language)で記述できる場合、SQL には含めないでください。 - クエリを作成する前に、対象期間について
get-trace-or-log-fieldsを呼び出してください。 - 複雑なクエリは、
execute-sqlの前にvalidate-sqlを呼び出し、返されたrewritten_sqlを必ず確認してください。 FROM logs/FROM tracesのみを使用し、signalと一致させてください。データベース名で修飾した名前は使用しないでください。- ストレージ列ではなく、カタログのラベルを使用してください。
levelではなくtype、pod_nameではなくinstance、app_serviceではなくservice、customer_identifierではなくdomainを使用します。 - トレースでは
duration_ms(ミリ秒)を使用してください。durationは拒否されます。analyze-tracesのナノ秒単位のしきい値を引き継がないでください。 timestampは、フィールド検出で一覧に表示されなくても、両方のシグナルで有効です。- 文字列値には、一重引用符を使用してください。SQL であり、
whereのミニ言語ではありません。 - 属性キーの検証は行われません。列がすべて空の場合は、データが欠落しているのではなく、キーが間違っています。
- 結果セットが空でも、それが答えです。クエリを書き換える前に対象期間を確認してください。
- SQL で集計してください。合計を計算するために行をページ分割して取得してはいけません。上限は 1000 行です。
- ログとトレースは別々のテーブルにあるため、1 つのステートメントでは結合できません。
- スコープ制限付きのロールで呼び出しが拒否された場合は、
search-*/analyze-*に切り替えてください。 - SQL で
bodyをそのままフィルターに使用しないでください。小文字にした検索語を使ってlower(body)にhasToken/hasAnyTokens/hasAllTokensを適用するか、lower(body) LIKE '%…%'を使用してください。それ以外の方法では、メッセージ列全体がスキャンされます。 bodyの検索は、一度に 1 つだけ、質問に答える最も狭い期間とフィルターで実行してください。並列実行はしないでください。