Azure DatabricksメトリックをAzure Log Analytics REST APIで取得する方法(KQL・Grafana・Java認証まで)

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点を押さえれば同じ結果を再現できます。

押さえるポイントやることよくある詰まり
KQLGrafana が実行しているKQLを取り出して再利用する「どのテーブル?どのクエリ?」が分からない
APILog 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推奨)
$__interval1m / 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ステップです。

  1. DefaultAzureCredential でアクセストークンを取得する
  2. HttpClient で Log Analytics Query API に POST し、JSONレスポンスをパースする

必要な依存関係(考え方)

最低限必要なのは次の2つです。

  • 認証:Azure Identity(DefaultAzureCredential)
  • JSON:Jackson など(レスポンスパース用)

さらに実務では「タイムアウト」「リトライ」「ログ」「メトリック化」も欲しくなりますが、まずは最小構成で動く形に寄せます。

環境変数でサービスプリンシパル認証させる(開発・検証に便利)

DefaultAzureCredential は、環境変数が入っていればサービスプリンシパルとして動きます。検証時は次の3つを設定しておくと早いです。

環境変数内容例
AZURE_TENANT_IDテナントIDxxxxxxxx-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 ForbiddenLog Analytics Reader 等を対象ワークスペースに付与
トークンのスコープ/リソースが違う403 InvalidTokenErrorLog 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パース)を詰めると、無駄なく確実にゴールへ到達できます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次