Laravel: Facade vs Contract

自分の言葉で言語化してメモ

Facade vs Contract

  • Facade と Contract はどちらも Laravel のコア機能を使う為の方法である

Facade

  • what
    • Laravelのコア機能をサービスコンテナを介さずに利用できる静的なインターフェース
  • メリット
    • サービスコンテナを意識することなくサービスに簡単にアクセスできる
  • デメリット
    • インジェクション(コンストラクタインジェクションまたはメソッドインジェクション)なしで利用できてしまう為、依存性の管理が難しくなり、テストが困難になる

※とはいえ、Facade も Laravel の内部でサービスコンテナに登録されてる
See: \Illuminate\Foundation\Application::registerCoreContainerAliases

Contract

  • what
    • Laravelのコア機能のインターフェース
    • 開発者は、ServiceProviderでContract(Interface)と実装クラスを明示的にbindする事で、サービスコンテナ経由でコア機能を利用する事ができる
  • メリット
    • 依存性の注入ができる為、疎結合になる
    • テストがしやすくなる
  • デメリット
    • サービスコンテナでのbindが必要

用語

Contract vs Interface

Laravelにおいて Contract(契約) という用語は、サービスコンテナに登録するために使用される Interface を指す

  • Contract (契約)
    • Laravelのコア機能のInterface
    • 言い換えると、サービスコンテナに登録するサービスのInterface

Azure Application Insights への特定のログ出力を Azure Monitor で監視し、Slack 通知する方法

先日、Azure Function の異常終了を検知し、Slack に通知する仕組みを構築しました。
今回は、Azure Application Insights に特定のログが出力されたら Slack に通知する仕組みを構築する手順を書きます。

使用するもの

  • Slack
    • 通知を受けるチャンネル
    • Incoming Webhook
  • Azure
    • Azure Application Insights ※エラーログの出力先
    • Azure Monitor ※エラーログを監視する
      • アクショングループ
      • アラートルール
    • Logic App ※エラーログ発生時に、Azure Monitor からトリガーし、Slack Incoming Webhook にメッセージを送信

概要

  1. Azure Function が異常終了すると Azure Function に severityLevel が 3 のログ(※1)が出力される
  2. Azure Monitor の アラートルール で上記のログの出力を監視
  3. ログを検知したら、アラートルール の アクショングループ から Locig App をトリガーする
    • アラートルールから直接、Slack の Incoming Webhook にメッセージを送信できない為、Locic Apps 経由で送信します
      • Logic Apps で Slack の Incoming Webhook に対応した payload を生成して送信する必要がある
  4. Logic Apps から Slack の Incoming Webhook にメッセージを送信

※1 のログを検索する KQL

traces | where operation_Name contains "対象Function名" 
  and message contains "Executed 'Functions.対象Function名'"
  and severityLevel != 1

構築手順

  1. Incoming Webhook の URL を取得
  2. (Azure Monitor から Logic App に送信されるペイロードを取得しておく)
    • Logic App 作成時に、要求本文のJSONスキーマ を設定します。この際、実際のペイロードを元に、スキーマを生成する為、予めペイロードを取得しておきます。
  3. Logic App を作成
  4. Azure Monitor の アクショングループ を作成
  5. Azure Monitor の アラートルール を作成

0. 監視対象の Azure Function を作成

  • 今回はログ監視の構築が主旨の為、Azure Portal 上でTimer Trigger の Function を作成し、5分毎に実行結果のログが Application Insights に出力されるようにしておきます

1. Incoming Webhook の URL を取得

  • 後ほど Logic App からの送信先に指定しますので、Webhook URL をコピーしておきます。

2. Azure Monitor から Logic App に送信されるペイロードを取得

  • ※後ほど、Logic Appのワークフローで送信するSlack通知メッセージに、アラート内容等を含める為に、 要求本文のJSONスキーマ の作成が必要になります

    • 要求本文のJSONスキーマ の作成に、実際のペイロードが必要な為、予め取得しておきます
  • 公式ドキュメントにペイロードのサンプルがあるので、どのサンプルが利用できれるかを予め把握できている場合は 手順 2. はスキップしてok です。

  • 取得方法としては、Logic Appを仮設定で作成し、アクショングループの作成(手順 4.)、及び、アラートルールの作成(手順 5.)を行い、一度、実際にペイロードを受信します

  • トリガーを HTTP要求の受信時 とし、要求本文のJSONスキーマ{} で作成します

    • (ここではペイロードが取得しただけなので) 以降のアクション(= HTTP の部分)は未設定でもok
  • (アラートルールからLogic Appがトリガーされるのを待つ)

  • Logic Appsの実行履歴より、ペイロードを取得します

  • 採取したペイロード

{
  "schemaId": "azureMonitorCommonAlertSchema",
  "data": {
    "essentials": {
      "alertId": "/subscriptions/c86f8ad8-7f32-4f84-8cf5-9ba182bd2a40/providers/Microsoft.AlertsManagement/alerts/ddc294f2-8510-62be-114c-4fd9e3300027",
      "alertRule": "TimerTrigger1関数のエラー終了",
      "severity": "Sev2",
      "signalType": "Log",
      "monitorCondition": "Fired",
      "monitoringService": "Log Alerts V2",
      "alertTargetIDs": [
        "/subscriptions/c86f8ad8-7f32-4f84-8cf5-9ba182bd2a40/resourcegroups/blog20231223/providers/microsoft.insights/components/sampleapptimertrigger"
      ],
      "configurationItems": [
        "/subscriptions/c86f8ad8-7f32-4f84-8cf5-9ba182bd2a40/resourceGroups/blog20231223/providers/microsoft.insights/components/SampleAppTimerTrigger"
      ],
      "originAlertId": "bef8427b-d266-440e-84be-4cbe66cd588e",
      "firedDateTime": "2023-12-23T03:23:02.1147771Z",
      "description": "TimerTrigger1関数がエラー終了しました",
      "essentialsVersion": "1.0",
      "alertContextVersion": "1.0"
    },
    "alertContext": {
      "properties": {},
      "conditionType": "LogQueryCriteria",
      "condition": {
        "windowSize": "PT1H",
        "allOf": [
          {
            "searchQuery": "traces | where operation_Name contains \"TimerTrigger1\" \nand message contains \"Executed 'Functions.TimerTrigger1'\"\nand severityLevel == 1",
            "metricMeasureColumn": null,
            "targetResourceTypes": "['microsoft.insights/components']",
            "operator": "GreaterThan",
            "threshold": "1",
            "timeAggregation": "Count",
            "dimensions": [],
            "metricValue": 8,
            "failingPeriods": {
              "numberOfEvaluationPeriods": 1,
              "minFailingPeriodsToAlert": 1
            },
            "linkToSearchResultsUI": "https://portal.azure.com#@6a02fd2f-7f98-4f16-9639-951b0994621f/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/source/Alerts.EmailLinks/scope/%7B%22resources%22%3A%5B%7B%22resourceId%22%3A%22%2Fsubscriptions%2Fc86f8ad8-7f32-4f84-8cf5-9ba182bd2a40%2FresourceGroups%2Fblog20231223%2Fproviders%2Fmicrosoft.insights%2Fcomponents%2FSampleAppTimerTrigger%22%7D%5D%7D/q/eJxVjDsOwkAMBXtOYW2TDikHSAkVokqPrM3TxhLrRbYDQeLwfKpQTTMzYZzh9KLHDAO1G4xDml7OXEG5abCoUxqlwkaTUmB9oh3rRBXuXLbWYUVeAhN1x0Xz9%2BP7v7JLv9Jxh0k8Tx9eaRiofwM%3D/prettify/1/timespan/2023-12-23T02%3a22%3a25.0000000Z%2f2023-12-23T03%3a22%3a25.0000000Z",
            "linkToFilteredSearchResultsUI": "https://portal.azure.com#@6a02fd2f-7f98-4f16-9639-951b0994621f/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/source/Alerts.EmailLinks/scope/%7B%22resources%22%3A%5B%7B%22resourceId%22%3A%22%2Fsubscriptions%2Fc86f8ad8-7f32-4f84-8cf5-9ba182bd2a40%2FresourceGroups%2Fblog20231223%2Fproviders%2Fmicrosoft.insights%2Fcomponents%2FSampleAppTimerTrigger%22%7D%5D%7D/q/eJxVjDsOwkAMBXtOYW2TDikHSAkVokqPrM3TxhLrRbYDQeLwfKpQTTMzYZzh9KLHDAO1G4xDml7OXEG5abCoUxqlwkaTUmB9oh3rRBXuXLbWYUVeAhN1x0Xz9%2BP7v7JLv9Jxh0k8Tx9eaRiofwM%3D/prettify/1/timespan/2023-12-23T02%3a22%3a25.0000000Z%2f2023-12-23T03%3a22%3a25.0000000Z",
            "linkToSearchResultsAPI": "https://api.applicationinsights.io/v1/apps/ec3b3196-ebf9-49af-814c-585faba48434/query?query=traces%20%7C%20where%20operation_Name%20contains%20%22TimerTrigger1%22%20%0Aand%20message%20contains%20%22Executed%20%27Functions.TimerTrigger1%27%22%0Aand%20severityLevel%20%3D%3D%201&timespan=2023-12-23T02%3a22%3a25.0000000Z%2f2023-12-23T03%3a22%3a25.0000000Z",
            "linkToFilteredSearchResultsAPI": "https://api.applicationinsights.io/v1/apps/ec3b3196-ebf9-49af-814c-585faba48434/query?query=traces%20%7C%20where%20operation_Name%20contains%20%22TimerTrigger1%22%20%0Aand%20message%20contains%20%22Executed%20%27Functions.TimerTrigger1%27%22%0Aand%20severityLevel%20%3D%3D%201&timespan=2023-12-23T02%3a22%3a25.0000000Z%2f2023-12-23T03%3a22%3a25.0000000Z"
          }
        ],
        "windowStartTime": "2023-12-23T02:22:25Z",
        "windowEndTime": "2023-12-23T03:22:25Z"
      }
    },
    "customProperties": {}
  }
}

3. Logic App を作成

  • プランの種類=消費 で作成します

  • ロジック アプリ デザイナー でワークフローを作成します

  • アラートルール からの HTTPリクエストをトリガーとする想定の為、HTTP 要求の受診時 を選択します

  • 上記で取得したペイロードを使用してスキーマを生成します

  • +新しいステップ を押下し、HTTP アクションを追加します

    • URI に Slack の Incoming Webhook のエンドポイントを指定
    • 本文 に、Slack Incoming Webhook のメッセージの送り方 (※2) に準拠したJSONを登録
      • メッセージに 動的なコンテンツの追加 よりdescriptionlinkToSearchResultsUI を含める事で、アラート概要と、当該KQLへのリンクをメッセージに含める事ができます
  • ※2 Slack Incoming Webhook のメッセージの送り方

  • 保存します

4. Azure Monitor の アクショングループ を作成

  • アクションタブより、上記で設定した Logic Apps をアラート発生時のアクションとして設定します

  • テストを実行し、Logic App 経由で Slack 通知が届く事を確認しておきます

5. Azure Monitor の アラートルール を作成

  • 監視対象の Function の 監視 > ログ より、Application Insights の画面に移動します

  • 監視対象となるログを検索し、+新しいアラートルール を押下します

※今回は severityLevel3 のログは出力されないので 1 を監視対象にしています

traces | where operation_Name contains "TimerTrigger1" 
and message contains "Executed 'Functions.TimerTrigger1'"
and severityLevel == 1
  • 以下の内容でアラートルールを作成します

  • 留意点

    • 以下の意味になります
      • 集計の粒度 = 過去何分間で出力されたログをアラート対象にするか
      • 評価の頻度 = 何分起きにログをチェックするか
    • 今回は、 集計の粒度 = 1時間、評価の頻度 = 5分 とし、過去1時間以内のログを、5分おきに監視します
  • アラート タブより、アラート検知時のアクションを、上記で作成したアクショングループに設定します

  • 詳細 タブより、アラートルール名 アラートルールの説明 を設定します

    • アラートルールの説明手順 3.動的なコンテンツ として取得できるので、 Slack 通知メッセージで利用します

Slack通知内容

  • エラーログを検知すると以下のSlack通知が飛ぶようになりました!
    • リンクから対象のエラーログに飛べます

EventGrid TriggerのAzure Functionをローカルで動かす方法

Azure Function のトリガーの種類により、ローカルでの実行方法が異なります。

  • HTTP トリガー

    • http://localhost:<PORT>/api/<FUNCTION_NAME> にリクエストする事で実行できます。
  • (EventGrid Trigger を除く) HTTP トリガー以外

  • EventGrid トリガー

    • EventGrid Trigger の URL (= http://localhost:7071/runtime/webhooks/eventgrid?functionName={FUNCTION_NAME}) にリクエストする事で実行できます。

ドキュメントはこちら
Core Tools を使用してローカルで Azure Functions を開発する - ローカル関数を実行する

EventGrid TriggerのAzure Functionをローカルで動かす方法 を以下に記載します。

バージョン

  • Azure Function Runtime Version: 4.25.3.21264
  • Azure Functions Core Tools: 4.0.5413 Commit hash: N/A (64-bit)

今回やった事

  1. Azure Functions extension for Visual Studio CodeEventGrid トリガーの Azure Function を作成
  2. Event Grid Viewerアプリ で EventGrid のイベントメッセージを取得
    • EventGrid Trigger の URL にリクエストする際、 Request Body でイベントメッセージを指定する必要がある為、予め実際のイベントメッセージを取得しておきます
  3. Azure Functions Core Toolsfunc コマンド で Azure Function をローカルでホスト
  4. EventGrid Trigger の URL にリクエストし、ローカルでホストしている Azure Function をトリガー

1. Azure Functions extension for Visual Studio CodeEventGrid トリガーの Azure Function を作成

今回は Azure Functions extension for Visual Studio Code を利用して Azure Function を作成します。

ドキュメント

手順

  1. VSCode > Azure Functions extension > WORCSPACE の Function アイコンより、Create New Project... を押下

  2. TypeScript, Model V4, Azure Event Grid Trigger と選択し、関数名を入力(今回は eventGridTriggerSample)し、Enterを押下すると、eventGridTriggerSample 関数 が作成される

2. Event Grid Viewer アプリ で EventGrid のイベントメッセージを取得

EventGrid Trigger の URL にリクエストする際、 Request Body でイベントメッセージを指定する必要がある為、予め実際のイベントメッセージを取得しておきます。

今回は Azure BLOB Storage Container へファイルがアップロードされた際のイベントメッセージを、Event Grid Viewerアプリで取得します 。

2-1. Event Grid Viewer アプリをデプロイ

Event Grid Viewer アプリ = ドキュメントの ビューアー Web アプリ の事。

EventGridのイベントメッセージをキャプチャする為の Event Grid Viewer アプリ を、learn.microsoft.com にあるARMテンプレートを利用して Azure AppService にカスタム デプロイします。

ドキュメント

手順

  1. learn.microsoft.com の ビューアー Web アプリを作成するより、Deploy to Azure ボタンを押下

  2. ブラウザで https://portal.azure.com/#create/Microsoft.Template が起動するので、サブスクリプション/リソースグループと、インスタンスの詳細を入力

  3. デプロイが成功すると以下のリソースが作成される

2-2. Event Grid の イベント サブスクリプション を作成

ドキュメント

手順

  1. ファイルのアップロード先となる Azure BLOB Storage Container を作成

    • ストレージアカウント を作成
    • ストレージアカウントデータストレージ: コンテナー (コンテナ名: upload) を作成
  2. Event Grid の イベント サブスクリプション を作成 (※1)

    • サブスクリプションEvent Grid リソース プロバイダー を登録
    • Event Grid システム トピック 及び イベント サブスクリプション を作成
      • イベント サブスクリプション > 「+ イベントブスクリプション」 より以下の内容で作成
        • トピックの種類
          • ストレージアカウント
        • ソースリソース
          • 上記で作成したストレージアカウント
        • エンドポイントのタイプ
          • webhook
        • エンドポイント ※イベントの送信先
          • https://{「Event Grid Viewer アプリ」のドメイン}/api/updates
            • 今回の例: https://viewerwebapptest1698469260.azurewebsites.net/api/updates

※1 ハマりどころ

Azure Portalのバグなのか Event Grid トピック > (対象のトピックを選択) > 「+ イベントブスクリプション」 からだとサブスクリプションが作成できなかった

2-3. Event Grid Viewer アプリ で EventGrid のイベントメッセージをキャプチャ

手順

  1. ブラウザで Event Grid Viewer アプリ を開いておく

  2. 上記で作成した Azure BLOB Storage Container にファイルをアップロード

  3. Event Grid Viewer アプリEvent Grid イベント メッセージ が届くので、Web上でメッセージ内容を確認

{
  "topic": "/subscriptions/c86f8ad8-7f32-4f84-8cf5-9ba182bd2a40/resourceGroups/event-grid-trigger-sample/providers/Microsoft.Storage/storageAccounts/storageaccount1698469260",
  "subject": "/blobServices/default/containers/upload/blobs/2023/10/28/test.txt",
  "eventType": "Microsoft.Storage.BlobCreated",
  "id": "1afcef29-f01e-0079-4e61-09740a066060",
  "data": {
    "api": "PutBlob",
    "clientRequestId": "50bdeec1-31cb-4fa4-b783-ea6fbb26400d",
    "requestId": "1afcef29-f01e-0079-4e61-09740a000000",
    "eTag": "0x8DBD778B179D29D",
    "contentType": "text/plain",
    "contentLength": 4,
    "blobType": "BlockBlob",
    "url": "https://storageaccount1698469260.blob.core.windows.net/upload/2023/10/28/test.txt",
    "sequencer": "0000000000000000000000000000EC5E000000000009cc7f",
    "storageDiagnostics": {
      "batchId": "3875e5f8-9006-0022-0061-094d31000000"
    }
  },
  "dataVersion": "",
  "metadataVersion": "1",
  "eventTime": "2023-10-28T05:42:37.6655517Z"
}

3. Azure Functions Core Toolsfunc コマンド で Azure Function をローカルでホスト

Azure Functions Core Toolsfunc コマンド で Azure Function をローカルで起動します

ドキュメント

手順

  • Azurite コンテナを起動

    • docker run -p 10000:10000 -p 10001:10001 mcr.microsoft.com/azure-storage/azurite
  • プロジェクトの直下に local.settings.json を以下の内容で設置

    • "AzureWebJobsStorage": "UseDevelopmentStorage=true" は azurite を使用する設定
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
  }
}
  • tsc で TypeScript を JavaScript に変換

    • yarn build
  • funcコマンド で eventGridTriggerSample 関数をローカルで起動

    • func start eventGridTriggerSample --typescript
$ func start eventGridTriggerSample --typescript

Azure Functions Core Tools
Core Tools Version:       4.0.5413 Commit hash: N/A  (64-bit)
Function Runtime Version: 4.25.3.21264

[2023-10-28T06:06:17.001Z] Worker process started and initialized.

Functions:

    eventGridTriggerSample: eventGridTrigger

For detailed output, run func with --verbose flag.
[2023-10-28T06:06:22.030Z] Host lock lease acquired by instance ID '000000000000000000000000816E9EDC'.

EventGrid Trigger の URL にリクエストし、ローカルでホストしている Azure Function をトリガー

EventGrid Trigger の URL を呼び出すことで EventGrid Trigger の Azure Function をローカルで実行する事ができます。

ドキュメント

EventGrid Trigger の URL

  • http://localhost:7071/runtime/webhooks/eventgrid?functionName={FUNCTION_NAME}

手順

  1. EventGrid Trigger の URL にリクエス
    • Request Body で上記で取得した 「EventGried の イベントメッセージ」を指定して POST
curl http://localhost:7071/runtime/webhooks/eventgrid?functionName=eventGridTriggerSample \
-H "Content-Type: application/json" \
-H "aeg-event-type: Notification" \
-d '{
  "topic": "/subscriptions/c86f8ad8-7f32-4f84-8cf5-9ba182bd2a40/resourceGroups/event-grid-trigger-sample/providers/Microsoft.Storage/storageAccounts/storageaccount1698469260",
  "subject": "/blobServices/default/containers/upload/blobs/2023/10/28/test.txt",
  "eventType": "Microsoft.Storage.BlobCreated",
  "id": "1afcef29-f01e-0079-4e61-09740a066060",
  "data": {
    "api": "PutBlob",
    "clientRequestId": "50bdeec1-31cb-4fa4-b783-ea6fbb26400d",
    "requestId": "1afcef29-f01e-0079-4e61-09740a000000",
    "eTag": "0x8DBD778B179D29D",
    "contentType": "text/plain",
    "contentLength": 4,
    "blobType": "BlockBlob",
    "url": "https://storageaccount1698469260.blob.core.windows.net/upload/2023/10/28/test.txt",
    "sequencer": "0000000000000000000000000000EC5E000000000009cc7f",
    "storageDiagnostics": {
      "batchId": "3875e5f8-9006-0022-0061-094d31000000"
    }
  },
  "dataVersion": "",
  "metadataVersion": "1",
  "eventTime": "2023-10-28T05:42:37.6655517Z"
}' --verbose

作業ログ: EventGrid Trigger の URL にリクエストし、eventGridTriggerSample 関数をトリガーした様子

$ curl http://localhost:7071/runtime/webhooks/eventgrid?functionName=eventGridTriggerSample \
> -H "Content-Type: application/json" \
> -H "aeg-event-type: Notification" \
> -d '{
>   "topic": "/subscriptions/c86f8ad8-7f32-4f84-8cf5-9ba182bd2a40/resourceGroups/event-grid-trigger-sample/providers/Microsoft.Storage/storageAccounts/storageaccount1698469260",
>   "subject": "/blobServices/default/containers/upload/blobs/2023/10/28/test.txt",
>   "eventType": "Microsoft.Storage.BlobCreated",
>   "id": "1afcef29-f01e-0079-4e61-09740a066060",
>   "data": {
>     "api": "PutBlob",
>     "clientRequestId": "50bdeec1-31cb-4fa4-b783-ea6fbb26400d",
>     "requestId": "1afcef29-f01e-0079-4e61-09740a000000",
>     "eTag": "0x8DBD778B179D29D",
>     "contentType": "text/plain",
>     "contentLength": 4,
>     "blobType": "BlockBlob",
>     "url": "https://storageaccount1698469260.blob.core.windows.net/upload/2023/10/28/test.txt",
>     "sequencer": "0000000000000000000000000000EC5E000000000009cc7f",
>     "storageDiagnostics": {
>       "batchId": "3875e5f8-9006-0022-0061-094d31000000"
>     }
>   },
>   "dataVersion": "",
>   "metadataVersion": "1",
>   "eventTime": "2023-10-28T05:42:37.6655517Z"
> }' --verbose
*   Trying 127.0.0.1:7071...
* Connected to localhost (127.0.0.1) port 7071 (#0)
> POST /runtime/webhooks/eventgrid?functionName=eventGridTriggerSample HTTP/1.1
> Host: localhost:7071
> User-Agent: curl/8.1.2
> Accept: */*
> Content-Type: application/json
> aeg-event-type: Notification
> Content-Length: 983
>
< HTTP/1.1 202 Accepted
< Content-Length: 0
< Date: Sat, 28 Oct 2023 06:28:51 GMT
< Server: Kestrel
<
* Connection #0 to host localhost left intact
~ $
  • eventGridTriggerSample 関数がトリガーされた
[2023-10-28T06:28:51.995Z] Executing 'Functions.eventGridTriggerSample' (Reason='EventGrid trigger fired at 2023-10-28T15:28:51.9435940+09:00', Id=ca807f6d-683c-4ddd-a68c-4e088fa4e650)
[2023-10-28T06:28:52.081Z] Event grid function processed event: {
[2023-10-28T06:28:52.081Z]   topic: '/subscriptions/c86f8ad8-7f32-4f84-8cf5-9ba182bd2a40/resourceGroups/event-grid-trigger-sample/providers/Microsoft.Storage/storageAccounts/storageaccount1698469260',
[2023-10-28T06:28:52.081Z]   subject: '/blobServices/default/containers/upload/blobs/2023/10/28/test.txt',
[2023-10-28T06:28:52.081Z]   eventType: 'Microsoft.Storage.BlobCreated',
[2023-10-28T06:28:52.081Z]   id: '1afcef29-f01e-0079-4e61-09740a066060',
[2023-10-28T06:28:52.081Z]   data: {
[2023-10-28T06:28:52.081Z]     api: 'PutBlob',
[2023-10-28T06:28:52.081Z]     clientRequestId: '50bdeec1-31cb-4fa4-b783-ea6fbb26400d',
[2023-10-28T06:28:52.081Z]     requestId: '1afcef29-f01e-0079-4e61-09740a000000',
[2023-10-28T06:28:52.081Z]     eTag: '0x8DBD778B179D29D',
[2023-10-28T06:28:52.081Z]     contentType: 'text/plain',
[2023-10-28T06:28:52.081Z]     contentLength: 4,
[2023-10-28T06:28:52.081Z]     blobType: 'BlockBlob',
[2023-10-28T06:28:52.081Z]     url: 'https://storageaccount1698469260.blob.core.windows.net/upload/2023/10/28/test.txt',
[2023-10-28T06:28:52.081Z]     sequencer: '0000000000000000000000000000EC5E000000000009cc7f',
[2023-10-28T06:28:52.081Z]     storageDiagnostics: { batchId: '3875e5f8-9006-0022-0061-094d31000000' }
[2023-10-28T06:28:52.081Z]   },
[2023-10-28T06:28:52.081Z]   dataVersion: '',
[2023-10-28T06:28:52.081Z]   metadataVersion: '1',
[2023-10-28T06:28:52.081Z]   eventTime: '2023-10-28T05:42:37.6655517Z'
[2023-10-28T06:28:52.081Z] }
[2023-10-28T06:28:52.112Z] Executed 'Functions.eventGridTriggerSample' (Succeeded, Id=ca807f6d-683c-4ddd-a68c-4e088fa4e650, Duration=155ms)

サンプル

CakePHP 4.x に ParaTest を導入

CakePHP 4.x に ParaTest を導入

テストの数が増えてきて全件テストを実行するのに時間がかかるようになった為、ParaTestを導入し、テストを並列実行するようにしてみました。

version

ドキュメント

ParaTest

  • PHPUnit でのテストを並列で実行できるツール
    • 並列数(プロセス数)はオプションで指定できる
    • テストクラス単位で並行実行される模様
  • 環境変数 ParaTest に並列実行されるプロセスごとに1から始まる連番が設定される
    • DB を利用したテストを並列実行する際は、TEST_TOKEN を利用して、各プロセス毎に利用するDBを分けるが必要がある
  • Laravel 10.x の並列テストでは内部的に ParaTest を使用

導入効果

元々 06:27 かかっていたところが 01:48 で実行できるようになりました

  • ParaTest (8 プロセス) : 01:48
  • PHPUnit (1 プロセス) : 06:27

CakePHP 4.x への導入方法のメモ

DB が絡むテストを並列実行する際は、TEST_TOKEN を利用して、各プロセス毎に利用するDBを分ける等の工夫が必要になる

  • 今回はCakePHPのコアのコードを読み、以下のようにする事で、各プロセス毎に利用するDBの出し分けを実現しました
    • Fixtureの調整あたりで少しハマりました

ParaTest をインストール

composer require --dev brianium/paratest --with-all-dependencies

並列数分のテスト用DBを作成する

  • 今回は 8 プロセスで並列実行する想定なので、並列数分のテスト用DBを作成
  • 作成方法は割愛

app_local.phpDatasources に ParaTest 用の connection の設定を追加

// app/config/app_local.php

$paraTestConnectionBase = [
    'className' => Connection::class,
    'driver' => //...,
    //...

    'host' => env('DB_HOST'),
    'port' => env('DB_PORT'),
    'username' => env('DB_USER'),
    'password' => env('DB_PASSWORD'),
    'encoding' => env('DB_ENCODING', 'UTF8'),
    'timezone' => env('DB_TIMEZONE', 'UTC'),
]

// ...

return [
    //...
    'Datasources' => [
        // ...

        // ParaTest用のconnection
        // note: connection名を `test_` で始まるものにする必要がある
        'test_1' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test1'],
        'test_2' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test2'],
        'test_3' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test3'],
        'test_4' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test4'],
        'test_5' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test5'],
        'test_6' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test6'],
        'test_7' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test7'],
        'test_8' => $paraTestConnectionBase + ['database' => env('DB_NAME').'_test8'],

TEST_TOKEN を元に、各プロセス毎の Connection の設定を調整

  • \Cake\TestSuite\Fixture\PHPUnitExtension::executeBeforeFirstTest の処理をカスタマイズする必要があった為、PHPUnitBeforeFirstTestHook を利用し、以下のようにカスタマイズ
// app/phpunit.xml.dist

//...
  <extensions>
    <extension class="Path\To\AppExtension"/>
    //...
// Path/To/AppExtension.php

//...
use PHPUnit\Runner\BeforeFirstTestHook;

//...
class AppExtension implements BeforeFirstTestHook
{
    /**
     * what:
     *  \Cake\TestSuite\Fixture\PHPUnitExtension::executeBeforeFirstTest の処理に、
     *  ParaTest 使用時の connection を調整する為の処理を追加したもの
     */
    public function executeBeforeFirstTest(): void
    {
        $helper = new ConnectionHelper();
        $helper->addTestAliases();

        // for ParaTest
        $paraTestToken = getenv('TEST_TOKEN');
        if ($paraTestToken !== false) {
            // ParaTestで各プロセス用のconnectionを設定する為に、
            // \Cake\TestSuite\ConnectionHelper::addTestAliases で設定した ConnectionManager::alias('test', 'default'); を上書き
            ConnectionManager::alias("test_{$paraTestToken}", 'default');
        }

        $enableLogging = in_array('--debug', $_SERVER['argv'] ?? [], true);
        if ($enableLogging) {
            $helper->enableQueryLogging();
            Log::drop('queries');
            Log::setConfig('queries', [
                'className' => 'Console',
                'stream' => 'php://stderr',
                'scopes' => ['queriesLog'],
            ]);
        }
    }

Fixture で利用する connection を調整

  • TEST_TOKEN を元に、当該プロセス用のconnectionを設定
// app/tests/Fixture/AppTestFixture.php

//...
use Cake\TestSuite\Fixture\TestFixture;

//...
class AppTestFixture extends TestFixture
{
    public function __construct()
    {
        // ParaTestの場合は、当該プロセス用のconnectionを設定
        $paraTestToken = getenv('TEST_TOKEN');
        if ($paraTestToken !== false) {
            $this->connection = "test_{$paraTestToken}";
        }

        parent::__construct();
    }
}
class XxxxFixture extends AppTestFixture

Test用DBのmigration方法を調整

  • こちらも TEST_TOKEN を元に当該プロセスで利用するDBをmigrateするように調整
  • 各DB毎に1回だけmigrateされるように調整
// app/tests/bootstrap.php

// Run migrations for multiple plugins
$migrator = new Migrator();

$paraTestToken = getenv('TEST_TOKEN');

// for ParaTest
if ($paraTestToken !== false) {
    $dirName = TMP . 'tests' . DS . 'paratest' . DS;
    $fileName = "migrated-test_{$paraTestToken}";
    $path = $dirName . $fileName;

    // 1DBにつき1回のみmigrate
    if (!file_exists($path)) {
        $DBName = "test_{$paraTestToken}";
        $connectionName = $DBName;

        $migrator->runMany([
            //...
            ['connection' => $connectionName, 'plugin' => '//...'],
        ]);

        if (!file_exists($dirName)) {
            mkdir($dirName);
        }
        touch($path);
    }
} else {
    $migrator->runMany([
        //...
        ['connection' => 'test', 'plugin' => '//...'],
    ]);
}

上記で作成した一時ファイルをParaTest実行前に削除するように設定

// composer.json

{
    //..
    "scripts": {
        //..
        "paratest": "rm -rf tmp/tests/paratest && ./vendor/bin/paratest -p 8",

ParaTest実行

composer paratest

CakePHP 4.x: Entityのカラム一覧は、DBのスキーマを参照して取得しており、取得結果は schema_cache としてキャッシュされている

CakePHP の実装

  • 実際のスキーマを元に、Entity 取得時に実行される SQL の SELECT 句に指定するカラム一覧を生成している
  • 上記は schema_cache としてキャッシュされる
    • bin/cake schema_cache clear した上で、Entity を取得した際、カラムの一覧を取得する SQL が実行される事を確認
    • bin/cake schema_cache clear せず、Entity を取得した際、カラムの一覧を取得する SQL が実行されない事を確認
  • migration ファイルで \Migrations\Table::update を実行時に schema_cache が再考構築される
    • これにより、通常は、カラム削除を伴う migration を実行しても問題が起こらない

ハマった事

  • BGデプロイメントで、カラム削除を伴う migration をデプロイした際に、ブルー側で、存在しないカラムを SELECT する SQL が実行されエラーになった
  • why
    • グリーン側で migration を行った後、ブルー側で bin/cake schema_cache clear していなかった為、schema_cache がクリアされず、CakePHP内部のカラム一覧と実際のカラム一覧に差異が生じた
    • 差異により、存在しないカラムを SELECT する SQL が実行された
  • 対策
    • 対象 Table クラスのコンストラクタで $this->getSchema()->removeColumn('対象カラム名') を実行する事で、schema_cache が古くても、「存在しないカラムの SELECT」を回避できる
    • migration のデプロイ前に、予め $this->getSchema()->removeColumn('対象カラム名') をデプロイしておく事で今回のエラーを回避できる
// 対象テーブルクラス

public function __construct(array $config = [])
{
    parent::__construct($config);

    // これにより、 `SELECT 削除予定のカラム, ...` が発生しなくなる
    $this->getSchema()->removeColumn('削除予定のカラム名');
}

ドキュメント

アプリケーションをデプロイする時にプラグインを使用する場合、 テーブルのカラムメタデータを更新するように、必ず ORM キャッシュをクリアしてください。 そうしなければ、それらの新しいカラムの操作を実行する時に、カラムが存在しないエラーになります。

M2 Mac で Apollo Twin X を使えるようにする方法

2023/02/11 の時点では、macmacOS 復旧 で起動し、“低セキュリティ ”オプション を設定する必要があります。

作業日

  • 2023/02/11

経緯

  • mac book pro 2016 (13 インチ) が突然故障し、再起不能となり、M2 Max の mac book pro (14インチ) を購入
  • 旧PCは起動できない状態
  • 新PCに、Time Machine をバックアップを復元した

備考

  • UADハードウェアのトランスファー が必要になる事を懸念していたが、トランスファーは必要なかった

Apollo Twin X を使えるようにする方法 (作業ログ)

  • まず、UAD Meter & Control Panel を再インストール
    1. こちら を参考にアンインストール
    2. https://www.uaudio.jp/downloads/uadmacOS用をダウンロード より UAD-v10.2.2-Mac.zip をダウンロードし、解答した中身の UAD-v10.2.2-Mac.pkg を実行し、再インストール  
  • UAD Meter & Control Panel に表示されるエラーに応じて、下記を実施

    • Driver not installed correctly が表示される場合

    • Connect UA Device が表示される場合

      • Apollo Twin Xmac に正しく接続されている事を確認 (ケーブルが抜けていてハマりました...)
    • Click Allow in Security & Privacy が表示される場合

      • システム設定 > プライバシーとセキュリティ > セキュリティ から 開発元"Universa Audio"のシステムソフトウェアがアップデートされました。許可 を押下
        • Ventura でシステム設定のUIが変更されているので、参考サイトを読む際は、スクショが古い事を念頭に置いて参考サイトを読む
  • 効果があったかどうかわからないこと

TODO

  • UAD Meter & Control Panel が正式に M2 Mac に対応したら、 “低セキュリティ ”オプション を解除すること!

教訓

  • Time Machine のバックアップを常時すること
    • 3 週間ほどバックアップをサボってしまい、その間に作成した譜面や、リハ/ライブの音源を消失しました...

PHPStorm で File Structure のショートカット(cmd + F12)が効かない事象の調査メモ

事象

  • cmd + F8(Breakpoints) などは効く
  • cmd + F12 だけ効かない
  • キーストロークは IDE に到達しますか?
    • => 到達しないので、他のプログラムまたはオペレーティングシステムがショートカットをインターセプト している

解消方法

今回のログ

https://twitter.com/idubmorgan/status/1545529993282797568