# 画面設計書 29-Worker Log（ワーカーログ）

## 概要

本ドキュメントは、ワーカーノードのエグゼキュータ/ドライバーのログファイルを表示する画面「Worker Log（ワーカーログ）」の設計書である。

### 本画面の処理概要

本画面は、Spark Standaloneクラスタのワーカーノード上で実行されるエグゼキュータやドライバーのログファイル、およびワーカー自身のログをWeb UI上で閲覧するための画面である。

**業務上の目的・背景**：Standaloneクラスタの運用において、エグゼキュータやドライバーのログを確認することは障害調査やパフォーマンス分析に不可欠である。通常はSSH等でワーカーノードに接続してログファイルを直接参照する必要があるが、本画面を使用することでWeb UIからリモートでログ内容を確認でき、運用効率を大幅に向上させることができる。

**画面へのアクセス方法**：以下の3つのアクセスパターンがある。
1. Worker Overview画面（No.28）のRunning/Finished Executorsテーブルのstdout/stderrリンクから遷移（appId + executorId指定）
2. Worker Overview画面のRunning/Finished Driversテーブルのstdout/stderrリンクから遷移（driverId指定）
3. Worker Overview画面のワーカーIDリンクから遷移（self指定 = ワーカー自身のログ）

**主要な操作・処理内容**：
1. エグゼキュータ/ドライバー/ワーカーのログファイルの指定範囲表示（デフォルトで末尾100KBを表示）
2. Load Moreボタンによる過去ログの追加読み込み
3. Load Newボタンによる最新ログの読み込み
4. バイト範囲の表示（表示中のバイト位置と総ファイルサイズ）
5. テキストAPI（/log）によるプレーンテキスト応答

**画面遷移**：
- 遷移元：Worker Overview画面（No.28）のログリンク、Application Detail画面（No.25）のログリンク
- 遷移先：Master Overview画面への「Back to Master」リンク

**権限による表示制御**：ログディレクトリのパスバリデーション（パストラバーサル防止）が行われる。ワーキングディレクトリ外のパスへのアクセスは拒否される。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 17 | Spark Web UI | 主機能 | ワーカーノードのExecutor/ドライバーのログファイルをページネーション付きで表示する主処理 |
| 84 | Standaloneクラスタマネージャ | 補助機能 | ワーカーのログディレクトリからExecutor/ドライバーのログファイルを取得 |

## 画面種別

詳細（ログビューア）

## URL/ルーティング

- **URL**: `/logPage/?{params}&logType={logType}`
- **HTTPメソッド**: GET
- **パラメータパターン**:
  - エグゼキュータログ: `/logPage/?appId={appId}&executorId={executorId}&logType={logType}`
  - ドライバーログ: `/logPage/?driverId={driverId}&logType={logType}`
  - ワーカー自身のログ: `/logPage/?self&logType={logType}`
- **追加パラメータ**:
  - `offset` - 表示開始バイト位置（任意）
  - `byteLength` - 表示バイト長（任意、デフォルト: 102400 = 100KB）
- **ページ名（WebUIPage）**: `logPage`
- **テキストAPI**: `/log?{params}&logType={logType}` （プレーンテキスト応答）

## 入出力項目

| 項目名 | 入出力 | 型 | 必須 | 説明 |
|--------|--------|------|------|------|
| appId | 入力（URLパラメータ） | String | 条件付き | アプリケーションID（エグゼキュータログ時必須） |
| executorId | 入力（URLパラメータ） | String | 条件付き | エグゼキュータID（エグゼキュータログ時必須） |
| driverId | 入力（URLパラメータ） | String | 条件付き | ドライバーID（ドライバーログ時必須） |
| self | 入力（URLパラメータ） | String | 条件付き | ワーカー自身のログであることを示すフラグ |
| logType | 入力（URLパラメータ） | String | 必須 | ログタイプ（stderr/stdout/out） |
| offset | 入力（URLパラメータ） | Long | 任意 | 表示開始バイト位置 |
| byteLength | 入力（URLパラメータ） | Int | 任意 | 表示バイト長（デフォルト: 102400） |

## 表示項目

### ログ表示セクション

| 項目名 | データソース | 説明 |
|--------|-------------|------|
| Showing {curLogLength} Bytes | endByte - startByte | 現在表示中のバイト数 |
| {startByte} - {endByte} | startByte, endByte | 表示範囲のバイト位置 |
| of {logLength} | logLength | ログファイルの総バイト数 |
| ログテキスト | getLog()の戻り値 | ログファイルの内容（pre要素で表示） |

### 操作ボタン

| ボタン名 | 説明 |
|----------|------|
| Load More | 過去のログを追加読み込み（上部に表示） |
| Load New | 最新のログを読み込み（下部に表示） |
| Back to Master | Master Overview画面に戻るリンク（worker.activeMasterWebUiUrl） |

## イベント仕様

### 1-ページ読み込み

1. URLパラメータから `appId`, `executorId`, `driverId`, `self` を取得する
2. パラメータの組み合わせからログディレクトリを決定する:
   - appId + executorId: `{workDir}/{appId}/{executorId}/`
   - driverId: `{workDir}/{driverId}/`
   - self: `{SPARK_LOG_DIR}/`（デフォルトは `{workDir}/`）
3. `logType` とその他のパラメータを取得する
4. `getLog(logDir, logType, offset, byteLength)` でログファイルの指定範囲を読み取る
5. ログテキスト、開始バイト、終了バイト、ログ総長を取得する
6. HTML要素を構成し、`initLogPage` JavaScript関数のパラメータとしてバイト情報を渡す

### 2-Load Moreボタン押下

JavaScriptの `loadMore()` 関数が呼び出され、`/log` テキストAPIから過去のログデータをAJAXで取得して画面上部に追加表示する。

### 3-Load Newボタン押下

JavaScriptの `loadNew()` 関数が呼び出され、`/log` テキストAPIから最新のログデータをAJAXで取得して画面下部に追加表示する。ログの末尾に達した場合は「End of Log」アラートが表示される。

### 4-テキストAPI

`/log` エンドポイントへのアクセス時、`renderLog` メソッドによりプレーンテキスト形式でログが返却される。バイト範囲のヘッダ情報付き。

## データベース更新仕様

### 操作別データベース影響一覧

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| ページ読み込み | ファイルシステム | READ | ログファイルの読み取り |

データベースへの更新は発生しない。ファイルシステムからのログ読み取りのみ。

## メッセージ仕様

| メッセージID | 種別 | メッセージ内容 | 発生条件 |
|-------------|------|--------------|----------|
| MSG-01 | 情報 | "End of Log" | Load New押下時にログの末尾に達した場合 |
| MSG-02 | エラー | "Error: Log type must be one of stderr, stdout, out" | サポートされていないlogTypeが指定された場合 |
| MSG-03 | エラー | "Error: invalid log directory {logDirectory}" | ワーキングディレクトリ外のパスが指定された場合（パストラバーサル防止） |
| MSG-04 | エラー | "Error getting logs due to exception: {message}" | ログファイル取得中に例外が発生した場合 |
| MSG-05 | エラー | "Request must specify either application or driver identifiers" | パラメータの組み合わせが不正な場合 |

## 例外処理

| 例外 | 発生条件 | 処理 |
|------|----------|------|
| 不正なパラメータ組み合わせ | appId/executorId/driverId/selfのいずれの有効な組み合わせにも該当しない場合 | Exceptionをスロー |
| 不正なlogType | stderr/stdout/out以外のlogTypeが指定された場合 | エラーメッセージを表示 |
| パストラバーサル検出 | 正規化されたログディレクトリがworkDir内にない場合 | エラーメッセージを表示 |
| ファイル読み取りエラー | ログファイルが存在しない、または読み取り権限がない場合 | エラーメッセージを表示、ログに記録 |

## 備考

- ログファイルは `RollingFileAppender.getSortedRolledOverFiles` でロールオーバーファイルを含めてソートされる
- デフォルトの表示バイト数は 100KB（defaultBytes = 100 * 1024）
- サポートされるログタイプは "stderr", "stdout", "out" の3種類
- "out" タイプの場合、`.out` 拡張子のファイルを検索する
- パストラバーサル防止として `Utils.isInDirectory(workDir, normalizedLogDir)` によるパスチェックが行われる
- ワーカー自身のログ（self指定）の場合、SPARK_LOG_DIR環境変数のディレクトリが参照される
- ページタイトルは "{logType} log page for {pageName}" であり、pageNameは "worker"（self）、"appId/executorId"（エグゼキュータ）、"driverId"（ドライバー）のいずれか
- テキストAPIの応答にはバイト範囲ヘッダ「==== Bytes {start}-{end} of {total} of {dir}{logType} ====」が付与される
- ログ表示領域は `height:80vh; overflow:auto` で画面の80%の高さにスクロール可能

---

## コードリーディングガイド

本画面を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

ログ取得結果のタプル構造とパラメータパターンを把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | LogPage.scala（worker） | `core/src/main/scala/org/apache/spark/deploy/worker/ui/LogPage.scala` | supportedLogTypes（35行目）: "stderr", "stdout", "out"。defaultBytes（36行目）: 100 * 1024。パラメータパターンマッチ（48-57行目, 74-83行目）: ログディレクトリ決定ロジック |

**読解のコツ**: ログディレクトリの決定は (appId, executorId, driverId, self) のOption型4タプルのパターンマッチで行われる。有効な組み合わせは3パターンのみ。

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | LogPage.scala（worker） | `core/src/main/scala/org/apache/spark/deploy/worker/ui/LogPage.scala` | renderメソッド（64-127行目）: パラメータ取得、ログディレクトリ決定、getLog呼び出し、HTML生成。renderLogメソッド（38-62行目）: テキストAPI用のプレーンテキスト応答 |

**主要処理フロー**:
1. **65-72行目**: URLパラメータ（appId, executorId, driverId, self, logType, offset, byteLength）の取得
2. **74-83行目**: パラメータパターンマッチでログディレクトリ・URLパラメータ文字列・ページ名を決定
3. **85行目**: `getLog(logDir, logType, offset, byteLength)` 呼び出し
4. **86行目**: `linkToMaster` （Back to Masterリンク）の生成
5. **108行目**: `logParams` にリクエストパラメータを構成
6. **109-110行目**: `initLogPage` JavaScript呼び出しコードの生成
7. **112-124行目**: HTML構成（linkToMaster, range, Load More, logText, alert, Load New）
8. **126行目**: `UIUtils.basicSparkPage` でHTMLレスポンス生成

#### Step 3: ログ取得処理の理解

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | LogPage.scala（worker） | `core/src/main/scala/org/apache/spark/deploy/worker/ui/LogPage.scala` | getLogメソッド（130-182行目）: サポートlogTypeチェック、パストラバーサル防止（Utils.isInDirectory）、RollingFileAppenderによるログファイル取得、バイト範囲読み取り |

**主要処理フロー**:
- **137-138行目**: logTypeがsupportedLogTypesに含まれるかチェック
- **142-144行目**: ログディレクトリのパス正規化とワーキングディレクトリ内チェック
- **150-155行目**: "out"タイプの場合、`.out`拡張子ファイルを検索
- **156行目**: `RollingFileAppender.getSortedRolledOverFiles` でロールオーバーファイル取得
- **159行目**: 各ファイルの長さ取得
- **161行目**: offset未指定時はファイル末尾からbyteLength分
- **173行目**: `Utils.offsetBytes` でバイト範囲読み取り

### プログラム呼び出し階層図

```
LogPage.render(request)                     [GET /logPage/]
    |
    +-- パラメータ取得（appId, executorId, driverId, self, logType等）
    |
    +-- ログディレクトリ決定（パターンマッチ）
    |       +-- (Some(a), Some(e), None, None) => workDir/a/e/
    |       +-- (None, None, Some(d), None)     => workDir/d/
    |       +-- (None, None, None, Some(_))     => SPARK_LOG_DIR/
    |
    +-- getLog(logDir, logType, offset, byteLength)
    |       |
    |       +-- supportedLogTypesチェック
    |       +-- Utils.isInDirectory(workDir, logDir)  [パストラバーサル防止]
    |       +-- RollingFileAppender.getSortedRolledOverFiles(logDir, fileName)
    |       +-- Utils.getFileLength(file, conf)
    |       +-- Utils.offsetBytes(files, fileLengths, startIndex, endIndex)
    |
    +-- HTML生成（range, moreButton, logText, alert, newButton）
    +-- initLogPage() JavaScript初期化
    +-- UIUtils.basicSparkPage(request, content, title)

LogPage.renderLog(request)                   [GET /log]
    |
    +-- パラメータ取得
    +-- ログディレクトリ決定
    +-- getLog(logDir, logType, offset, byteLength)
    +-- プレーンテキスト応答（バイト範囲ヘッダ付き）
```

### データフロー図

```
[入力]                       [処理]                           [出力]

URLパラメータ            ───> LogPage.render                 ───> HTML Page
(appId, executorId,           |                                  |
 driverId, self,              |                                  +-- バイト範囲表示
 logType, offset)             |                                  +-- ログテキスト (pre)
                              |                                  +-- Load More/New ボタン
workDir                  ───> ログディレクトリ決定
SPARK_LOG_DIR                 |
                              +-- パストラバーサルチェック
ファイルシステム               |
  +-- RollingFileAppender ───> getLog()                     ───> (logText, start, end, total)
      ログファイル群           |
                         offsetBytes()
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| LogPage.scala | `core/src/main/scala/org/apache/spark/deploy/worker/ui/LogPage.scala` | ソース | ワーカーログ画面のメインページクラス |
| WorkerWebUI.scala | `core/src/main/scala/org/apache/spark/deploy/worker/ui/WorkerWebUI.scala` | ソース | ページ・ハンドラ登録（logPage, /logエンドポイント） |
| WorkerPage.scala | `core/src/main/scala/org/apache/spark/deploy/worker/ui/WorkerPage.scala` | ソース | ワーカー概要画面（遷移元） |
| RollingFileAppender.scala | `core/src/main/scala/org/apache/spark/util/logging/RollingFileAppender.scala` | ソース | ロールオーバーログファイル管理 |
| Utils.scala | `core/src/main/scala/org/apache/spark/util/Utils.scala` | ソース | offsetBytes, getFileLength, isInDirectoryメソッド |
| utils.js | `core/src/main/resources/org/apache/spark/ui/static/utils.js` | JavaScript | initLogPage, loadMore, loadNew 関数の実装 |
| UIUtils.scala | `core/src/main/scala/org/apache/spark/ui/UIUtils.scala` | ソース | HTML生成ユーティリティ |
