Azure Databricks のログを Azure Log Analytics に送って Grafana で可視化できているのに、同じメトリックを自作プログラム(Java)から取ろうとすると「どのKQLを投げる?」「REST APIの呼び出し方は?」「Bearerトークンが403になる…」で止まりがちです。この記事では、Grafana ダッシュボードJSONに埋まっているKQLを再利用し、Log Analytics クエリAPIをHTTPで呼び出してメトリックを取得するまでを、手順・注意点・Java実装例までまとめて解説します。
全体像:Grafanaで見えているなら「同じKQL+同じワークスペース」に投げれば取れる
結論から言うと、Grafana で Azure Monitor(Log Analytics)をデータソースにして Databricks メトリックを可視化できている場合、すでに「メトリックを作る元データ(Log Analytics のテーブル)」と「メトリック定義(KQL)」が揃っています。自作アプリ側では、次の3点を押さえれば同じ結果を再現できます。
| 押さえるポイント | やること | よくある詰まり |
|---|---|---|
| KQL | Grafana が実行しているKQLを取り出して再利用する | 「どのテーブル?どのクエリ?」が分からない |
| API | Log Analytics クエリREST APIにPOSTする | エンドポイント、body、timespanの指定が不明 |
| 認証 | Log Analytics 向けスコープでアクセストークンを取得しBearerで送る | Try it で403(InvalidTokenError / TokenExpired) |
以下、順に掘り下げます。
前提チェック:Log Analytics に Databricks のログが入っているか確認する
まず「APIは正しいのに結果が空(またはテーブルが存在しない)」を避けるため、Log Analytics の画面(Logs)で最低限の疎通を確認します。Databricks の監視構成(公式手順どおりに Azure Monitor / Log Analytics に送信済み)であれば、カスタムログのテーブルが _CL で終わる名前で作られていることが多いです(例:SparkListenerEvent_CL)。
Log Analytics の「Logs」で、まずはテーブル候補を探します。
// 直近のデータが入っているテーブルをざっくり洗い出す例
search *
| where TimeGenerated > ago(1h)
| summarize Count=count() by $table
| top 20 by Count desc
候補が見つかったら、最小クエリで中身を確認します。
// 例:SparkListenerEvent_CL があるなら中身確認
SparkListenerEvent_CL
| where TimeGenerated > ago(30m)
| take 10
この段階でテーブルが見つからない場合は、送信設定やワークスペースの選択、診断設定の宛先(別ワークスペースになっていないか)を疑います。テーブルが見つかり、データが見えたなら次に進みます。
Grafanaが使っているKQLを取り出して、そのまま再利用する
「どのKQLを投げればGrafanaと同じメトリックが取れるのか?」の答えはシンプルで、Grafanaが投げているKQLをそのまま使うのが最短です。Databricks 監視のサンプルでは、Grafana ダッシュボードの JSON テンプレート内に Log Analytics へ投げるKQLが定義されています。
GrafanaダッシュボードJSONからKQLを見つける方法
Grafana では、ダッシュボードの設定画面から JSON(JSON model / JSONを表示)を開けます。そこに Azure Monitor(Log Analytics)向けのクエリが入っています。探し方は次のどれかが確実です。
- 検索キーワード:
azureLogAnalytics/query/workspace/SparkListenerEvent_CL - JSONパスの目安:
targets[].azureLogAnalytics.query
例として、JSONのイメージは次のような形です(あくまで例なので、実際のキー名はダッシュボードによって多少異なります)。
{
"targets": [
{
"refId": "A",
"azureLogAnalytics": {
"workspace": "YOUR_WORKSPACE_ID",
"query": "let results=SparkListenerEvent_CL\r\n| where $__timeFilter(TimeGenerated)\r\n| where ...\r\n| summarize ... by bin(TimeGenerated, $__interval)\r\n"
}
}
]
}
ポイントは、Grafanaはこの query に書かれたKQLを実行してメトリックを作っているということです。つまり、自作アプリでもこのKQLを基準にすれば「同じメトリック」に最短距離で到達します。
注意:Grafanaのマクロ($__timeFilter など)は自作API呼び出しではそのまま通らない
Grafana のKQLには、ダッシュボードの時間範囲や間引き間隔を埋め込むための「マクロ」が含まれることがあります。代表例が $__timeFilter(TimeGenerated) や $__interval です。これは Grafana 側が実行時に実値へ置換してから Log Analytics に送るため、自作アプリでそのまま投げると構文エラーになりがちです。
自作アプリで再利用する場合は、次のどちらかで対応します。
| 方法 | メリット | デメリット |
|---|---|---|
| Grafanaマクロを自分で置換してKQLを完成させる | Grafanaと同じロジックをほぼ完全再現できる | 時間範囲・intervalの置換ロジックを自分で持つ必要がある |
APIの timespan を使い、KQL側は ago() 等で固定化する | 実装が簡単、運用しやすい | Grafanaと完全一致しない(ダッシュボード側の粒度と差が出る可能性) |
よく使うマクロの置換イメージは次のとおりです。
| Grafanaマクロ | 置換例(考え方) | 補足 |
|---|---|---|
$__timeFilter(TimeGenerated) | TimeGenerated between (datetime(2025-12-19T00:00:00Z) .. datetime(2025-12-19T01:00:00Z)) | from/to は自作アプリで決める(UTC推奨) |
$__interval | 1m / 5m / 1h など | bin(TimeGenerated, 5m) の粒度に影響 |
「まずは取れることが最優先」なら、最初は ago(30m) などに固定して動かし、あとでGrafana互換の置換ロジックを足すのが現実的です。
Log Analytics クエリ REST API でメトリックを取得する
Grafana と同じ(または互換にした)KQLが準備できたら、次はREST APIで実行します。使うのは Azure Monitor / Azure Log Analytics のクエリAPIです。
エンドポイント
ワークスペースID(GUID)を使って次のエンドポイントにPOSTします。
https://api.loganalytics.io/v1/workspaces/<ワークスペースID>/query
ここでいう「ワークスペースID」は、Azure Portal の Log Analytics ワークスペースの概要にある Workspace ID(GUID)です。ワークスペース名とは別物なので注意してください。
リクエスト(body/headers)
基本は次の2つだけ押さえれば動きます。
- body:
queryにKQLを入れる(必要ならtimespanを付ける) - headers:
Authorization: Bearer ...とContent-Type: application/json
例(timespanで直近1時間を指定するパターン):
{
"query": "SparkListenerEvent_CL | where TimeGenerated > ago(1h) | summarize Count=count()",
"timespan": "PT1H"
}
例(from/to を KQL に埋め込むパターン):
{
"query": "SparkListenerEvent_CL | where TimeGenerated between (datetime(2025-12-19T00:00:00Z) .. datetime(2025-12-19T01:00:00Z)) | summarize Count=count()"
}
HTTPヘッダ例:
Authorization: Bearer <アクセストークン>
Content-Type: application/json
レスポンス形式(JSON)
レスポンスは「テーブル形式」です。最初はここが戸惑いポイントですが、構造は一定です。
tables配列の中に結果テーブルが入る- 各テーブルは
columns(列定義)とrows(値)を持つ
イメージ:
{
"tables": [
{
"name": "PrimaryResult",
"columns": [
{ "name": "TimeGenerated", "type": "datetime" },
{ "name": "Count", "type": "long" }
],
"rows": [
["2025-12-19T00:00:00Z", 123],
["2025-12-19T00:01:00Z", 98]
]
}
]
}
Grafana が描画している「時系列グラフ」は、多くの場合 KQL 側で summarize ... by bin(TimeGenerated, 1m) のように整形され、ここに時系列点が返っています。自作アプリでは、この rows をパースして任意の形式(自社監視基盤、Prometheus push、CSV、別DB投入など)に変換できます。
403(InvalidTokenError / TokenExpired)の原因と、正しいBearerトークンの作り方
ドキュメントの「Try it」からREST APIを叩くと 403 が出るケースは珍しくありません。典型パターンは次のいずれかです。
| 症状 | 原因 | 対策 |
|---|---|---|
| 403 Forbidden / TokenExpired | トークンの有効期限切れ | 自分でトークンを取得し、期限内に更新する(SDK利用が楽) |
| 403 Forbidden / InvalidTokenError | トークンのaudience(リソース/スコープ)が違う | Log Analytics 用のリソース/スコープで取得する |
| 403 Forbidden(メッセージが権限系) | ワークスペースにRBAC権限がない | Log Analytics Reader 等を付与する |
安定運用するなら、アプリ側でアクセストークンを取得して呼び出すのが確実です。方法は大きく2つあります。
認証方式の選び方(マネージドID vs サービスプリンシパル)
| 方式 | 向いている環境 | メリット | 注意点 |
|---|---|---|---|
| マネージドID(推奨) | Azure上で動くアプリ(VM/AKS/App Service/Functions等) | シークレット管理が不要、漏洩リスクが低い | 実行基盤にID付与+ワークスペースにロール付与が必要 |
| サービスプリンシパル(アプリ登録) | オンプレ/他クラウド/ローカル検証、またはID制約がある場合 | どこでも動かせる、検証が速い | クライアントシークレットの安全管理が必須(Key Vault等) |
どちらの方式でも、Log Analytics ワークスペースに対して「読む権限」が必要です。一般的には、対象のワークスペース(または上位のリソースグループ/サブスクリプション)に対して Log Analytics Reader 相当のロールを付けます。組織のロール設計によっては Monitoring Reader を使う場合もありますが、目的は「Logsのクエリが通る権限」を付けることです。
サービスプリンシパルでのトークン取得(curl例)
検証として分かりやすいのが client_credentials フローです。質問内容のとおり、次のようなコマンドでトークンを取得し、そのトークンを Bearer として付けると呼び出せます。
curl -X POST \
-d "grant_type=client_credentials" \
-d "client_id=<クライアントID>" \
-d "client_secret=<クライアントシークレット>" \
-d "resource=https://api.loganalytics.io/" \
https://login.microsoftonline.com/<テナントID>/oauth2/token
返ってきたJSONの access_token を取り出し、API呼び出しに使います。
Authorization: Bearer <access_token>
ここで重要なのが resource が https://api.loganalytics.io/ になっている点です。これが別のリソース(例:management.azure.com 等)だと、トークンの宛先が一致せず 403 になりやすくなります。
Azure Identity SDK(DefaultAzureCredential)でのトークン取得:スコープは「https://api.loganalytics.io/.default」
Javaでの実装を安定させるなら、Azure Identity SDK for Java を使い、DefaultAzureCredential でトークンを取るのが実務的です。DefaultAzureCredential は、環境変数・マネージドID・開発環境ログインなど、利用可能な資格情報を順に試し、最終的に使えるものを選んでくれます。
このとき Log Analytics 用のスコープとして、次を指定します。
https://api.loganalytics.io/.default
「Try it は動かないのに自作は動く」状態を作るには、自分が制御できる方法でトークンを発行すること、そしてスコープ/リソースを Log Analytics に合わせることが重要です。
まずは最短で疎通:curl でクエリを投げて結果が返るところまで確認する
Java実装に入る前に、REST APIの要素(ワークスペースID、KQL、Bearerトークン)が揃っているかを curl で確認しておくと、切り分けが一気に楽になります。
curl -X POST \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"query":"SparkListenerEvent_CL | where TimeGenerated > ago(30m) | summarize Count=count()"}' \
https://api.loganalytics.io/v1/workspaces/<WORKSPACE_ID>/query
ここで結果が返れば、あとは Java で同じことをするだけです。もし失敗する場合は、次の観点で切り分けます。
- 401系:ヘッダにBearerがない/間違い/トークン不正
- 403系:権限不足、またはトークンの宛先(resource/scope)が違う
- 400系:KQL構文エラー、存在しないテーブル/列、マクロ未置換
Javaでの実装:DefaultAzureCredential+HTTPでLog Analytics Query APIを呼び出す
ここからが本題です。構成は次の2ステップです。
- DefaultAzureCredential でアクセストークンを取得する
- HttpClient で Log Analytics Query API に POST し、JSONレスポンスをパースする
必要な依存関係(考え方)
最低限必要なのは次の2つです。
- 認証:Azure Identity(
DefaultAzureCredential) - JSON:Jackson など(レスポンスパース用)
さらに実務では「タイムアウト」「リトライ」「ログ」「メトリック化」も欲しくなりますが、まずは最小構成で動く形に寄せます。
環境変数でサービスプリンシパル認証させる(開発・検証に便利)
DefaultAzureCredential は、環境変数が入っていればサービスプリンシパルとして動きます。検証時は次の3つを設定しておくと早いです。
| 環境変数 | 内容 | 例 |
|---|---|---|
AZURE_TENANT_ID | テナントID | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
AZURE_CLIENT_ID | クライアントID(アプリケーションID) | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
AZURE_CLIENT_SECRET | クライアントシークレット | (機密情報) |
本番運用では、シークレットを環境変数に直置きせず、Key Vault 連携やマネージドIDへ移行する設計が一般的です。
Javaサンプル:トークン取得→クエリ実行→結果の取り出し
以下は「HTTP+KQLで取りたい」という要件に合わせた、最小の実装例です。実際には例外処理やログ出力を強化してください。
import com.azure.core.credential.AccessToken;
import com.azure.core.credential.TokenRequestContext;
import com.azure.identity.DefaultAzureCredential;
import com.azure.identity.DefaultAzureCredentialBuilder;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class LogAnalyticsQueryExample {
// Log Analytics ワークスペースID(GUID)
private static final String WORKSPACE_ID = "<WORKSPACE_ID>";
// Log Analytics Query API endpoint
private static final String ENDPOINT =
"https://api.loganalytics.io/v1/workspaces/" + WORKSPACE_ID + "/query";
public static void main(String[] args) throws Exception {
// 1) トークン取得(Log Analytics 用スコープ)
DefaultAzureCredential credential = new DefaultAzureCredentialBuilder().build();
TokenRequestContext trc = new TokenRequestContext()
.addScopes("https://api.loganalytics.io/.default");
// getToken は非同期。簡単のため block() を使う(本番は非同期設計も検討)
AccessToken token = credential.getToken(trc).block();
if (token == null) {
throw new IllegalStateException("アクセストークンの取得に失敗しました。認証設定を確認してください。");
}
String accessToken = token.getToken();
// 2) KQL(例:直近30分の件数)
// GrafanaのKQLを流用する場合は、$__timeFilter 等のマクロを置換してから入れる
String kql = "SparkListenerEvent_CL | where TimeGenerated > ago(30m) | summarize Count=count()";
// 3) body(JSON)
// timespan を付ける場合は {\"query\":\"...\",\"timespan\":\"PT30M\"} のようにする
String requestBody = "{"
+ "\"query\":\"" + escapeJson(kql) + "\""
+ "}";
// 4) HTTP POST
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(ENDPOINT))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + accessToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("Log Analytics query failed. status="
+ response.statusCode() + " body=" + response.body());
}
// 5) 結果パース
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(response.body());
JsonNode tables = root.get("tables");
if (tables == null || !tables.isArray() || tables.size() == 0) {
System.out.println("結果が空です。KQLまたは時間範囲を確認してください。");
return;
}
JsonNode primary = tables.get(0);
JsonNode columns = primary.get("columns");
JsonNode rows = primary.get("rows");
System.out.println("== columns ==");
for (JsonNode c : columns) {
System.out.println(c.get("name").asText() + " (" + c.get("type").asText() + ")");
}
System.out.println("== rows ==");
for (JsonNode r : rows) {
System.out.println(r.toString());
}
}
// JSONに埋め込むための最低限のエスケープ
private static String escapeJson(String s) {
return s.replace("\\\\", "\\\\\\\\")
.replace("\"", "\\\\\"")
.replace("\n", "\\\\n")
.replace("\r", "");
}
}
このサンプルが動けば、次は「GrafanaのKQLをコピペして置換したもの」に差し替えるだけで、同じメトリックに寄せられます。
実務でのコツ:KQLはコードに直書きせず「設定ファイル化」すると運用が楽
GrafanaのダッシュボードJSONが更新されると、KQLも微妙に変わることがあります。自作アプリ側で保守しやすくするなら、KQLを次のように扱うのがおすすめです。
- KQLは 外部ファイル(.kql) や 設定(ConfigMap/Parameter Store) に置く
- Grafanaマクロ置換のために、from/to/interval をパラメータで受け取れるようにする
- 変更管理(Git)して「どのKQLでどの指標を出しているか」を追えるようにする
こうしておくと、監視要件の変更(集計粒度や列名変更、テーブル移行)が入ってもアプリの再ビルド無しで追従しやすくなります。
Grafana互換を高める:よくあるKQLパターンと、API向けの書き方
Databricks(Spark)監視でよく出てくるKQLは、概ね次の形に収束します。
- 対象テーブル(例:
SparkListenerEvent_CL) - 時間フィルタ(
TimeGenerated) - 必要なイベントやクラスター識別子で絞り込み
- 時系列化(
bin(TimeGenerated, 1m)) - 集計(
summarize)
例:時系列の件数(まずはこれが動くと安心)
// 直近1時間、1分粒度で件数推移
SparkListenerEvent_CL
| where TimeGenerated > ago(1h)
| summarize Count=count() by bin(TimeGenerated, 1m)
| order by TimeGenerated asc
例:テーブルのスキーマ(列名)が分からないとき
カスタムログは列名が環境で違うことがあります。まずは take で列を見てから組み立てるのが堅実です。
SparkListenerEvent_CL
| where TimeGenerated > ago(30m)
| take 5
例:Json文字列をパースして中の値を取り出す
ログの1列にJSONが入っている場合、parse_json() と tostring()/toint() などで取り出します。列名は実データに合わせて読み替えてください。
// 例:Event_s にJSONが入っている想定
SparkListenerEvent_CL
| where TimeGenerated > ago(1h)
| extend e = parse_json(Event_s)
| extend eventType = tostring(e.Event)
| summarize Count=count() by eventType
| order by Count desc
この「まずは取れる」「次に整形する」「最後にGrafanaと合わせる」の順で進めると、KQLの迷子になりにくいです。
よくある落とし穴:Workspace ID、RBAC、マクロ未置換、テーブル名違い
最後に、ハマりやすい点をチェックリストとしてまとめます。
| 落とし穴 | 症状 | 対処 |
|---|---|---|
| ワークスペース「名前」と「ID」を混同 | 404 / 400 / 403、または常に空結果 | Overview の Workspace ID(GUID)を使う |
| RBACが不足 | 403 Forbidden | Log Analytics Reader 等を対象ワークスペースに付与 |
| トークンのスコープ/リソースが違う | 403 InvalidTokenError | Log Analytics 用に https://api.loganalytics.io/(resource)または https://api.loganalytics.io/.default(scope)で発行 |
| Grafanaマクロ未置換 | 400 BadRequest(KQL構文エラー) | $__timeFilter や $__interval を実値に置換する |
| テーブル名が環境で違う | 「テーブルが存在しない」系エラー | search * でテーブルを洗い出し、実際の _CL テーブル名に合わせる |
発展:HTTP直叩きより「Azure Monitor Query SDK」を使う選択肢もある
要件が「HTTP+KQLで取得したい」ならここまでの方法で十分ですが、実務では次の理由でSDK利用も有力です。
- 認証・リトライ・例外整形などのボイラープレートが減る
- 実装が読みやすくなり、将来のAPI差分にも追従しやすい
- チーム開発で「誰が見ても分かる」形に寄せやすい
ただし、GrafanaのKQLを完全流用したい場合は、結局KQL自体の保守が核になるため「KQLをどこで管理するか(設定化)」が重要という点は変わりません。
まとめ:GrafanaのKQLを起点にすれば、同じメトリックを自作アプリでも再現できる
- メトリック定義(KQL)は、GrafanaダッシュボードJSONの
azureLogAnalytics.queryから取り出して再利用する - 取得は Log Analytics クエリ REST API(
https://api.loganalytics.io/v1/workspaces/<ID>/query)にPOSTする - 認証はマネージドIDまたはサービスプリンシパルで行い、Log Analytics向けのスコープ/リソースでトークンを発行して Bearer で送る
- Javaでは DefaultAzureCredential を使うとトークン取得を標準化でき、403やTry it依存の不安定さを回避しやすい
まずは「curlで200が返る」「Javaで同じKQLが動く」までを最短で通し、そのあと Grafanaマクロ互換やメトリックの整形(bin粒度、列名、JSONパース)を詰めると、無駄なく確実にゴールへ到達できます。

コメント