Tauri でロギングする公式プラグイン Tauri Log Plugin

# はじめに

Tauri は、そのフレームワークの構造故にバックエンドを Rust で書く。それが故、Rust でのロギング事情を調べるというのもあるが、Tauri そのものの構造を利用した上でのロギングとはどいう言うものかを調べたくなった。
また、Tauri はフロントエンドが Webview で構成されているため、そちらのコンソール出力をどう受け取るかも気になった。
そこで調べていると、Tauri の[プラグインワークスペース](https://github.com/tauri-apps/plugins-workspace)に [Tauri Log Plugin](https://github.com/tauri-apps/plugins-workspace/tree/v1/plugins/log) というものがあることに気付いた。
今回はこれをきっかけに Tauri でのログの取り方を学んでみたいと思う。

https://github.com/tauri-apps/plugins-workspace/tree/v1/plugins/log

### 環境

* macOS Sonoma 14.2.1
* Windows 11 Pro 23H2
* Tauri 1.5.4
* Rust 1.17.0

# Tauri Log Plugin を使用する

まずは、Tauri 公式のプラグインから。

クレートを依存関係に追加する。

```toml:Cargo.toml
[dependencies]
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
```

:::note info
**Tauri v2 への対応**
Tauri は、まもなく Tauri 2 リリースを控えている様子。そして、Tauri v2 ではプラグインへのサポートも手厚くなっている。
そうした状況から、Tauri Log Plugin も　[v2 が控えている](https://docs.rs/tauri-plugin-log/2.0.0-beta.2/tauri_plugin_log/)。
v2 では、設定のし方が変更になっているため、また正式リリース時に試して追記していこうと思う。
:::

## 基本セットアップ

```rust:main.rs
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use tauri_plugin_log::LogTarget;

fn main() {
    tauri::Builder::default()
        .plugin(
            tauri_plugin_log::Builder::new()
                .targets([
                    LogTarget::Stdout, 
                    LogTarget::Webview,
                    LogTarget::LogDir
                ]).build(),
        )
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

まず、Tauri に対して `.plugin()` を使用して、プラグインを登録する。

次に、`.targets({})` を使用し、どのターゲットのログを取るのかを選択する。
ターゲットのログは `LogTarget::Stdout`, `LogTarget::Stderr`, `LogTarget::Webview`, `LogTarget::LogDir`、`LogTarget::Folder(PathBuf)` がある。

| タイプ | 用途 |
|:-:|:-:|
|Stdout|標準出力のログを取る。|
|Stderr|標準エラー出力のログを取る。|
|Webview|Webview側 (フロントエンド) の出力ログを取る。|
|LogDir|規定のディレクトリパス以下にログファイルを保存する。（以下参照）|
|Folder(PathBuf)|指定のディレクトリパス以下にログファイルを保存する。|


`LogTarget::LogDir` を使用すると、ローカルファイルにログを書き出してくれるが、デフォルトの保存場所は以下の様になっている。

| OS | Template | Example |
|:-:|:-:|:-:|
|Windows|`{configDir}/{bundleIdentifier}`|`C:\Users\Alice\AppData\Roaming\com.tauri.dev`|
|Linux|`{configDir}/{bundleIdentifier}`|`/home/alice/.config/com.tauri.dev`/home/alice/.config/com.tauri.dev`|
|macOS|`{homeDir}/Library/Logs/{bundleIdentifier}`|`/Users/Alice/Library/Logs/com.tauri.dev`|

## ログを取る

ログを取る方法はシンプルで、 `log` クレートを使用する。

```powershell
cargo add log
```

### バックエンド側

```diff_rust:main.rs
fn main() {
    tauri::Builder::default()
        .plugin(
            tauri_plugin_log::Builder::new()
                .targets([
                    LogTarget::Stdout,
                    LogTarget::Webview,
                    LogTarget::LogDir
                ])
                .build(),
        )
        .setup(|app| {
            // 起動時に以下のログを取る
+           info!("This is info message!");
+           warn!("This is warning message!");
+           error!("this is error message!");
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

![スクリーンショット 2024-03-16 2.49.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/d958e33d-c4e7-5f23-5dbf-748994f992ed.png)

### フロントエンド側

まず依存モジュールをインストールする。

```
npm i https://github.com/tauri-apps/tauri-plugin-log#v1
```

依存モジュールである `tauri-plugin-log-api` の中から、それぞれログレベルごとの関数とコンソール出力と連結できる `attachConsole` を引用してくることが出来る。

```svelte:+page.svelte
<script lang="ts">
    import { info, warn, trace, error, debug, attachConsole } from 'tauri-plugin-log-api';

    // こちらを実行しておく事で Webview 側のコンソール出力にも表示される様になる。
    // 又、返り値の dettach を使用する事で、再度 Webview のコンソール出力を切り離すことが出来る。
    const dettach = attachConsole();

    trace('this trace log from frontend')
	info('this info log from frontend');
	warn('this warn log from frontend');
	error('this error log from frontend');
	debug('this debug log from frontend');
</script>
```

![Code_2024-03-19_11-29-46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/645a6fda-6067-b4cc-fa1c-d59284b4105a.png)


:::note info
**Webview のモジュール名**
Webview のモジュール名は `webview::*` となる。
:::


## ログレベルの設定

Tauri Log Plugin では、ログレベルの設定は 2 種類ある。

* `.level(LogFilter)`
* `.level_for(module, LogFilter)`

違いは、`.level_for(module, LogFilter)` はモジュール単位でのフィルタもできる。
なので、[コンフィグモジュール](https://qiita.com/takavfx/items/5c27d22df50be45a8968) でのログレベル設定だけ行うといったこともできる。

```diff_rust:main.rs
mod config;

use log::{error, info, warn, LevelFilter};
use tauri::{generate_handler, Manager};
use tauri_plugin_log::LogTarget;

fn main() {
    let app_state = config::AppState::new();

    tauri::Builder::default()
        .manage(app_state)
        .plugin(
            tauri_plugin_log::Builder::new()
                .targets([
                    LogTarget::Stdout,
                    LogTarget::Webview,
                    LogTarget::LogDir])
+               .level(LevelFilter::Info)
+               .level_for("app::config", LevelFilter::Trace)
                .build(),
        )
        .invoke_handler(generate_handler![
            config::commands::get_language,
            config::commands::set_language
        ])
        .setup(|app| {
            info!("This is info message!");
            warn!("This is warning message!");
            error!("this is error message!");

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

```diff_rust:config.rs
use log::{debug, info};
use std::sync::Mutex;

#[allow(dead_code)]
pub struct AppState {
    language: Mutex<String>,
}

impl Default for AppState {
    fn default() -> Self {
        Self {
            language: Mutex::from("en".to_string()),
        }
    }
}

impl AppState {
    pub fn new() -> Self {
        Self::default()
    }
}

pub mod commands {
    use super::*;

    #[tauri::command]
    pub async fn get_language(
        state: tauri::State<'_, AppState>
    ) -> Result<String, String> {
        let language = state.language.lock().unwrap().clone();
+       debug!("Current language is `{}`.", language);
        Ok(language)
    }

    #[tauri::command]
    pub async fn set_language(
        state: tauri::State<'_, AppState>,
        new_lang: String,
    ) -> Result<(), String> {
        *state.language.lock().unwrap() = new_lang;
+       info!("Set language to `{}`", *state.language.lock().unwrap());
        Ok(())
    }
}

```

```svelte:src/routes/+page.svelte
<script lang="ts">
    import { info, warn, trace, error, debug, attachConsole } from 'tauri-plugin-log-api';
    import { invoke } from '@tauri-apps/api'

    const dettach = attachConsole();

    trace('this trace log from frontend')
    info('this info log from frontend');
    warn('this warn log from frontend');
    error('this error log from frontend');
    debug('this debug log from frontend');

    // ページ表示時に言語情報を取得してプリント
	$: invoke('get_language').then((res) => {
		console.log(res);
	});

	async function onClicked() {
        // 言語を設定する別の機能の呼び出しコマンド
		await invoke('set_language', { newLang: 'ja' });
		console.log('Button clicked.');
	}
</script>

<button on:click={onClicked}>Click!!</button>
```


![スクリーンショット 2024-03-16 6.37.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/df046732-76f9-838a-a7f4-ce04ca41621b.png)
![Code_2024-03-19_11-27-03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/5eb0b897-1ee1-6ca9-a822-1b1e5f18fd92.png)


:::note info
**config 名について**
ログレベルをフィルタする際に、config の値を文字列で入力することになるが、上記のように `app::*` となることに気をつけておく。
今回の場合は `app::config` としてあったが、さらに `app::config::commands` といったスコープ指定もできる。
又、先の通り Webview のモジュール名は `webview::*` となる。
:::

## タイムゾーンを設定する

デフォルトではログに使用されるタイムゾーンは UTC になる。
ローカルタイムゾーンを使用する場合は別途設定が必要。

タイムゾーンの設定には、`.timezone_strategy(TimezoneStrategy)` に指定 Enum タイプを渡す。
タイプは `UseUtc` か `UseLocal` があり、ローカルタイムでログを取る場合は `UseLocal` を使用する。

```diff_rust:main.rs
mod config;

use log::{error, info, warn, LevelFilter};
use tauri::generate_handler;
-use tauri_plugin_log::LogTarget;
+use tauri_plugin_log::{LogTarget, TimezoneStrategy};

fn main() {
    let app_state = config::AppState::new();

    tauri::Builder::default()
        .manage(app_state)
        .plugin(
            tauri_plugin_log::Builder::new()
                .targets([LogTarget::Stdout, LogTarget::Webview, LogTarget::LogDir])
                .level(LevelFilter::Info)
                .level_for("app::config", LevelFilter::Trace)
+               .timezone_strategy(TimezoneStrategy::UseLocal)
                .build(),
        )
        .invoke_handler(generate_handler![
            config::commands::get_language,
            config::commands::set_language
        ])
        .setup(|_app| {
            info!("This is info message!");
            warn!("This is warning message!");
            error!("this is error message!");

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

## ログファイル保存のオプション

### ファイル分割オプション

ログファイルへの保存 (Rotation) には2通りのオプションがある。

* `KeepAll`
    * すべてのログを保存
    * 随時ログが溜められるファイルは `[log_name].log` として保存される
    * 最大容量を超えると `[log_name]_[year]-[month]-[day]_[hour]-[minute]-[second].log` 形式で保存される
* `KeepOne`
    * **デフォルト**
    * 単一ログファイルのみを保存。最大ログファイルサイズになった場合、ファイル削除され作成し直される

設定する場合は `.rotation_strategy(RotationStrategy)` を使用する。

```diff_rust:main.rs
mod config;

use log::{error, info, warn, LevelFilter};
use tauri::generate_handler;
-use tauri_plugin_log::{LogTarget, TimezoneStrategy};
+use tauri_plugin_log::{LogTarget, TimezoneStrategy, RotationStrategy};

fn main() {
    let app_state = config::AppState::new();

    tauri::Builder::default()
        .manage(app_state)
        .plugin(
            tauri_plugin_log::Builder::new()
                .targets([LogTarget::Stdout, LogTarget::Webview, LogTarget::LogDir])
                .level(LevelFilter::Info)
                .level_for("app::config", LevelFilter::Trace)
                .timezone_strategy(TimezoneStrategy::UseLocal)
+               .rotation_strategy(RotationStrategy::KeepAll)
                .build(),
        )
        .invoke_handler(generate_handler![
            config::commands::get_language,
            config::commands::set_language
        ])
        .setup(|_app| {
            info!("This is info message!");
            warn!("This is warning message!");
            error!("this is error message!");

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

### ファイルサイズオプション

ファイル保存の最大ファイルサイズを設定する。
設定する場合は `.max_file_size(u128)` を使用する。

デフォルトは `40000` (Byte)。 

この最大ファイルサイズを超えると、上記 RotationStrategy に則り、[ファイル分割を行う](https://github.com/tauri-apps/plugins-workspace/blob/6db4320e98a4278d605dd05d4d87e1433939a7cd/plugins/log/src/lib.rs#L387)。

```diff_rust:main.rs
mod config;

use log::{error, info, warn, LevelFilter};
use tauri::generate_handler;
use tauri_plugin_log::{LogTarget, TimezoneStrategy, RotationStrategy};

fn main() {
    let app_state = config::AppState::new();

    tauri::Builder::default()
        .manage(app_state)
        .plugin(
            tauri_plugin_log::Builder::new()
                .targets([LogTarget::Stdout, LogTarget::Webview, LogTarget::LogDir])
                .level(LevelFilter::Info)
                .level_for("app::config", LevelFilter::Trace)
                .timezone_strategy(TimezoneStrategy::UseLocal)
                .rotation_strategy(RotationStrategy::KeepAll)
+               .max_file_size(100000)
                .build(),
        )
        .invoke_handler(generate_handler![
            config::commands::get_language,
            config::commands::set_language
        ])
        .setup(|_app| {
            info!("This is info message!");
            warn!("This is warning message!");
            error!("this is error message!");

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```


## ログ名を設定する

カスタムのログ名 (log_name) を設定する。
設定するには `.log_name()` を使用する。

ログ名は、ログファイル (`[log_name].log` or `[log_name]_[year]-[month]-[day]_[hour]-[minute]-[second].log`) にも使用される。

デフォルトでは Tauri アプリの名前が当てられる。

```diff_rust:main.rs
mod config;

use log::{error, info, warn, LevelFilter};
use tauri::generate_handler;

fn main() {
    let app_state = config::AppState::new();

    tauri::Builder::default()
        .manage(app_state)
        .plugin(
            tauri_plugin_log::Builder::new()
                .targets([LogTarget::Stdout, LogTarget::Webview, LogTarget::LogDir])
+                .log_name("MyApp")
                .build(),
        )
        .invoke_handler(generate_handler![
            config::commands::get_language,
            config::commands::set_language
        ])
        .setup(|_app| {
            info!("This is info message!");
            warn!("This is warning message!");
            error!("this is error message!");

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```


# まとめ

今回は Tauri Log Plugin をどう設定すればいいかに注力してまとめてみた。
ただ、もうちょっと深く入りこんで見る必要もありそうなので、また機能や気づき等を見つけたら追記していきたい。