Go での生成 AI アプリケーションの実用的なオブザーバビリティ手法

1. 概要

生成 AI アプリケーションには、他のアプリケーションと同様にオブザーバビリティが必要です。生成 AI には特別なオブザーバビリティ手法が必要ですか?

このラボでは、シンプルな生成 AI アプリケーションを作成します。Cloud Run にデプロイします。Google Cloud のオブザーバビリティ サービスとプロダクトを使用して、重要なモニタリング機能とロギング機能を実装します。

学習内容

  • Cloud Shell エディタで Vertex AI を使用するアプリケーションを作成する
  • アプリケーション コードを GitHub に保存する
  • gcloud CLI を使用して、アプリケーションのソースコードを Cloud Run にデプロイする
  • 生成 AI アプリケーションにモニタリング機能とロギング機能を追加する
  • ログベースの指標を使用する
  • Open Telemetry SDK によるロギングとモニタリングの実装
  • 責任ある AI データ処理に関する分析情報を取得する

2. 前提条件

Google アカウントをまだお持ちでない場合は、新しいアカウントを作成する必要があります。

3. プロジェクトの設定

  1. Google アカウントを使用して Google Cloud コンソールにログインします。
  2. 新しいプロジェクトを作成するか、既存のプロジェクトを再利用します。作成または選択したプロジェクトのプロジェクト ID をメモします。
  3. プロジェクトの課金を有効にします
    • このラボの完了にかかる料金は $5 未満です。
    • このラボの最後にある手順に沿ってリソースを削除すると、それ以上の請求が発生しなくなります。
    • 新規ユーザーは、300 米ドル分の無料トライアルをご利用いただけます。
  4. Cloud 課金システムの [マイ プロジェクト] で課金が有効になっていることを確認します。
    • 新しいプロジェクトの Billing account 列に Billing is disabled と表示されている場合:
      1. [Actions] 列のその他アイコンをクリックします。
      2. [お支払い情報を変更] をクリックします。
      3. 使用する請求先アカウントを選択します。
    • ライブ イベントに参加している場合、アカウントの名前は Google Cloud Platform 無料トライアルの請求先アカウント になる可能性があります。

4. Cloud Shell エディタを準備する

  1. Cloud Shell エディタに移動します。認証情報を使用して gcloud を呼び出すことを Cloud Shell に承認するよう求めるメッセージが表示されたら、[承認] をクリックして続行します。
    クリックして Cloud Shell を承認する
  2. ターミナル ウィンドウを開く
    1. ハンバーガー メニュー ハンバーガー メニュー アイコン をクリックします。
    2. [Terminal] をクリックします。
    3. [New Terminal
      ] をクリックします。Cloud Shell エディタで新しいターミナルを開く
  3. ターミナルで、プロジェクト ID を構成します。
    gcloud config set project [PROJECT_ID]
    
    [PROJECT_ID] は、プロジェクトの ID に置き換えます。たとえば、プロジェクト ID が lab-example-project の場合、コマンドは次のようになります。
    gcloud config set project lab-project-id-example
    
    gcloud が GCPI API への認証情報をリクエストしているというメッセージが表示されたら、[承認] をクリックして続行します。
    クリックして Cloud Shell を承認する
    実行が成功すると、次のようなメッセージが表示されます。
    Updated property [core/project].
    
    WARNING が表示され、Do you want to continue (Y/N)? を求められた場合は、プロジェクト ID が正しく入力されていない可能性があります。正しいプロジェクト ID が見つかったら、N キー、Enter キーを押して、gcloud config set project コマンドをもう一度実行してみてください。
  4. (省略可)プロジェクト ID が見つからない場合は、次のコマンドを実行して、作成日が降順で並べ替えられたすべてのプロジェクトのプロジェクト ID を確認します。
    gcloud projects list \
         --format='value(projectId,createTime)' \
         --sort-by=~createTime
    

5. Google API を有効にする

ターミナルで、このラボに必要な Google API を有効にします。

gcloud services enable \
     run.googleapis.com \
     cloudbuild.googleapis.com \
     aiplatform.googleapis.com \
     logging.googleapis.com \
     monitoring.googleapis.com \
     cloudtrace.googleapis.com

このコマンドは完了するまでに時間がかかることがあります。最終的に、次のような成功メッセージが表示されます。

Operation "operations/acf.p2-73d90d00-47ee-447a-b600" finished successfully.

ERROR: (gcloud.services.enable) HttpError accessing で始まり、次のようなエラーの詳細を含むエラー メッセージが表示された場合は、1 ~ 2 分待ってからコマンドを再試行します。

"error": {
  "code": 429,
  "message": "Quota exceeded for quota metric 'Mutate requests' and limit 'Mutate requests per minute' of service 'serviceusage.googleapis.com' ...",
  "status": "RESOURCE_EXHAUSTED",
  ...
}

6. 生成 AI Go アプリケーションを作成する

このステップでは、Gemini モデルを使用して、選択した動物に関する 10 個の興味深い情報を表示する、シンプルなリクエストベースのアプリケーションのコードを記述します。次の手順でアプリケーション コードを作成します。

  1. ターミナルで codelab-o11y ディレクトリを作成します。
    mkdir ~/codelab-o11y
    
  2. 現在のディレクトリを codelab-o11y に変更します。
    cd ~/codelab-o11y
    
  3. Go モジュールを初期化します。
    go mod init codelab
    
  4. Vertex AI SDK for Go をインストールします。
    go get cloud.google.com/go/vertexai/genai
    
  5. Go 用のメタデータ ライブラリをインストールして、現在のプロジェクト ID を取得します。
    go get cloud.google.com/go/compute/metadata
    
  6. setup.go ファイルを作成し、Cloud Shell エディタで開きます。
    cloudshell edit setup.go
    
    初期化コードをホストするために使用されます。setup.go という名前の新しい空のファイルがエディタ ウィンドウに表示されます。
  7. 次のコードをコピーして、開いた setup.go ファイルに貼り付けます。
    package main
    
    import (
        "context"
        "os"
    
        "cloud.google.com/go/compute/metadata"
    )
    
    func projectID(ctx context.Context) (string, error) {
        var projectID = os.Getenv("GOOGLE_CLOUD_PROJECT")
        if projectID == "" {
               return metadata.ProjectIDWithContext(ctx)
        }
        return projectID, nil
    }
    
  8. ターミナル ウィンドウに戻り、次のコマンドを実行して Cloud Shell エディタで main.go ファイルを作成して開きます。
    cloudshell edit main.go
    
    ターミナルの上にあるエディタ ウィンドウに空のファイルが表示されます。画面は次のようになります。
    main.go の編集を開始した後に Cloud Shell エディタを表示する
  9. 次のコードをコピーして、開いた main.go ファイルに貼り付けます。
    package main
    
    import (
        "context"
        "fmt"
        "net/http"
        "os"
    
        "cloud.google.com/go/vertexai/genai"
    )
    
    var model *genai.GenerativeModel
    
    func main() {
        ctx := context.Background()
        projectID, err := projectID(ctx)
        if err != nil {
            return
        }
    
        var client *genai.Client
        client, err = genai.NewClient(ctx, projectID, "us-central1")
        if err != nil {
            return
        }
        defer client.Close()
           model = client.GenerativeModel("gemini-1.5-flash-001")
           http.HandleFunc("/", Handler)
           port := os.Getenv("PORT")
        if port == "" {
            port = "8080"
        }
        if err := http.ListenAndServe(":"+port, nil); err != nil {
            return
        }
    }
    
    func Handler(w http.ResponseWriter, r *http.Request) {
        animal := r.URL.Query().Get("animal")
        if animal == "" {
            animal = "dog"
        }
    
        prompt := fmt.Sprintf("Give me 10 fun facts about %s. Return the results as HTML without markdown backticks.", animal)
        resp, err := model.GenerateContent(r.Context(), genai.Text(prompt))
        if err != nil {
            w.WriteHeader(http.StatusTooManyRequests)
            return
        }
    
        if len(resp.Candidates) > 0 && len(resp.Candidates[0].Content.Parts) > 0 {
            htmlContent := resp.Candidates[0].Content.Parts[0]
            w.Header().Set("Content-Type", "text/html; charset=utf-8")
            fmt.Fprint(w, htmlContent)
        }
    }
    
    数秒後、Cloud Shell エディタによってコードが自動的に保存されます。

生成 AI アプリケーションのコードを Cloud Run にデプロイする

  1. ターミナル ウィンドウでコマンドを実行して、アプリケーションのソースコードを Cloud Run にデプロイします。
    gcloud run deploy codelab-o11y-service \
         --source="${HOME}/codelab-o11y/" \
         --region=us-central1 \
         --allow-unauthenticated
    
    コマンドで新しいリポジトリが作成されることを知らせるプロンプトが表示されます。[Enter] をクリックします。
    Deploying from source requires an Artifact Registry Docker repository to store built containers.
    A repository named [cloud-run-source-deploy] in region [us-central1] will be created.
    
    Do you want to continue (Y/n)?
    
    デプロイ プロセスには数分かかることがあります。デプロイ プロセスが完了すると、次のような出力が表示されます。
    Service [codelab-o11y-service] revision [codelab-o11y-service-00001-t2q] has been deployed and is serving 100 percent of traffic.
    Service URL: https://codelab-o11y-service-12345678901.us-central1.run.app
    
  2. 表示された Cloud Run サービスの URL を、ブラウザの別のタブまたはウィンドウにコピーします。または、ターミナルで次のコマンドを実行してサービス URL を出力し、表示された URL をクリックして Ctrl キーを押しながら URL を開きます。
    gcloud run services list \
         --format='value(URL)' \
         --filter='SERVICE:"codelab-o11y-service"'
    
    URL を開くと、500 エラーが発生したり、次のようなメッセージが表示されたりすることがあります。
    Sorry, this is just a placeholder...
    
    サービスがデプロイを完了していないことを意味します。しばらく待ってからページを更新します。最後に、犬に関する面白い情報で始まるテキストが表示され、犬に関する 10 の面白い情報が記載されています。

アプリを操作して、さまざまな動物に関する豆知識を調べてみましょう。そのためには、animal パラメータを URL に追加します(?animal=[ANIMAL] のようにします。ここで、[ANIMAL] は動物の名前です)。たとえば、?animal=cat を追加すると猫に関する 10 個の豆知識が、?animal=sea turtle を追加するとウミガメに関する 10 個の豆知識が返されます。

7. Vertex API 呼び出しを監査する

Google API 呼び出しを監査すると、「特定の API を呼び出したのは誰か、いつ、どこで呼び出したか」などの質問に答えることができます。監査は、アプリケーションのトラブルシューティング、リソース使用量の調査、ソフトウェアのフォレンジック分析を行う際に重要です。

監査ログを使用すると、管理アクティビティとシステム アクティビティを追跡できるほか、「データ読み取り」API オペレーションと「データ書き込み」API オペレーションの呼び出しをログに記録できます。コンテンツを生成するための Vertex AI リクエストを監査するには、Cloud コンソールで「データ読み取り」監査ログを有効にする必要があります。

  1. 下のボタンをクリックして、Cloud コンソールの [監査ログ] ページを開きます。

  2. このラボ用に作成したプロジェクトがページで選択されていることを確認します。選択したプロジェクトは、ページの左上にあるハンバーガー メニューに表示されます。
    Google Cloud コンソールのプロジェクト プルダウン
    必要に応じて、コンボボックスから正しいプロジェクトを選択します。
  3. [データアクセス監査ログの構成] テーブルの [サービス] 列で、Vertex AI API サービスを見つけて、サービス名の左側にあるチェックボックスをオンにして、サービスを選択します。
    Vertex AI API を選択します。
  4. 右側の情報パネルで、監査タイプとして [データ読み取り] を選択します。
    データ読み取りログを確認する
  5. [保存] をクリックします。

監査ログを生成するには、サービスの URL を開きます。?animal= パラメータの値を変更しながらページを更新して、異なる結果を取得します。

監査ログを確認する

  1. 次のボタンをクリックして、Cloud コンソールでログ エクスプローラ ページを開きます。

  2. 次のフィルタを [クエリ] ペインに貼り付けます。
    LOG_ID("cloudaudit.googleapis.com%2Fdata_access") AND
    protoPayload.serviceName="aiplatform.googleapis.com"
    
    クエリペインは、ログ エクスプローラ ページの上部にあるエディタです。
    監査ログにクエリを実行する
  3. [クエリを実行] をクリックします。
  4. 監査ログエントリのいずれかを選択し、フィールドを開いてログにキャプチャされた情報を調べます。
    Vertex API 呼び出しの詳細(使用されたメソッドやモデルなど)を確認できます。また、呼び出し元の ID と、呼び出しを承認した権限も確認できます。

8. 生成 AI とのインタラクションをロギングする

監査ログに API リクエスト パラメータやレスポンス データが記録されていません。ただし、この情報は、アプリケーションとワークフローの分析のトラブルシューティングに役立ちます。このステップでは、アプリケーション ロギングを追加してこのギャップを埋めます。ロギングでは、標準の Go log/slog パッケージを使用して構造化ログを書き込みます。log/slog パッケージは、Google Cloud にログを書き込む方法を認識していません。標準出力への書き込みをサポートしています。ただし、Cloud Run には、標準出力に出力された情報をキャプチャして Cloud Logging に自動的に取り込む機能があります。構造化ログを正しく取得するには、出力されるログを適切にフォーマットする必要があります。以下の手順に沿って、Go アプリケーションに構造化ロギング機能を追加します。

  1. ブラウザの [Cloud Shell] ウィンドウ(またはタブ)に戻ります。
  2. ターミナルで setup.go を再度開きます。
    cloudshell edit ~/codelab-o11y/setup.go
    
  3. コードを、ロギングを設定するバージョンに置き換えます。コードを置き換えるには、ファイルの内容を削除し、次のコードをコピーしてエディタに貼り付けます。
    package main
    
    import (
    	"context"
    	"os"
    	"log/slog"
    	"cloud.google.com/go/compute/metadata"
    )
    
    func projectID(ctx context.Context) (string, error) {
        var projectID = os.Getenv("GOOGLE_CLOUD_PROJECT")
        if projectID == "" {
               return metadata.ProjectIDWithContext(ctx)
        }
        return projectID, nil
    }
    
    func setupLogging() {
        opts := &slog.HandlerOptions{
            Level: slog.LevelDebug,
            ReplaceAttr: func(group []string, a slog.Attr) slog.Attr {
                switch a.Key {
                case slog.LevelKey:
                    a.Key = "severity"
                    if level := a.Value.Any().(slog.Level); level == slog.LevelWarn {
                        a.Value = slog.StringValue("WARNING")
                    }
                case slog.MessageKey:
                    a.Key = "message"
                case slog.TimeKey:
                    a.Key = "timestamp"
                }
                return a
            },
        }
        jsonHandler := slog.NewJSONHandler(os.Stdout, opts)
        slog.SetDefault(slog.New(jsonHandler))
    }
    
  4. ターミナルに戻り、main.go を再度開きます。
    cloudshell edit ~/codelab-o11y/main.go
    
  5. アプリケーション コードを、モデルとのインタラクションをログに記録するバージョンに置き換えます。コードを置き換えるには、ファイルの内容を削除し、次のコードをコピーしてエディタに貼り付けます。
    package main
    
    import (
        "context"
        "fmt"
        "net/http"
        "os"
    
        "encoding/json"
        "log/slog"
    
        "cloud.google.com/go/vertexai/genai"
    )
    
    var model *genai.GenerativeModel
    
    func main() {
        ctx := context.Background()
        projectID, err := projectID(ctx)
        if err != nil {
            return
        }
    
        setupLogging()
    
        var client *genai.Client
        client, err = genai.NewClient(ctx, projectID, "us-central1")
        if err != nil {
            slog.ErrorContext(ctx, "Failed to marshal response to JSON", slog.Any("error", err))
            os.Exit(1)
        }
        defer client.Close()
        model = client.GenerativeModel("gemini-1.5-flash-001")
        http.HandleFunc("/", Handler)
        port := os.Getenv("PORT")
        if port == "" {
            port = "8080"
        }
        if err := http.ListenAndServe(":"+port, nil); err != nil {
            slog.ErrorContext(ctx, "Failed to start the server", slog.Any("error", err))
            os.Exit(1)
        }
    }
    
    func Handler(w http.ResponseWriter, r *http.Request) {
        animal := r.URL.Query().Get("animal")
        if animal == "" {
            animal = "dog"
        }
    
        prompt := fmt.Sprintf("Give me 10 fun facts about %s. Return the results as HTML without markdown backticks.", animal)
        resp, err := model.GenerateContent(r.Context(), genai.Text(prompt))
        if err != nil {
            w.WriteHeader(http.StatusTooManyRequests)
            return
        }
    
        jsonBytes, err := json.Marshal(resp)
        if err != nil {
            slog.Error("Failed to marshal response to JSON", slog.Any("error", err))
        } else {
            slog.DebugContext(r.Context(), "content is generated", slog.String("animal", animal),
                slog.String("prompt", prompt), slog.String("response", string(jsonBytes)))
        }
    
        if len(resp.Candidates) > 0 && len(resp.Candidates[0].Content.Parts) > 0 {
            htmlContent := resp.Candidates[0].Content.Parts[0]
            w.Header().Set("Content-Type", "text/html; charset=utf-8")
            fmt.Fprint(w, htmlContent)
        }
    }
    

ロギングは、ログを stdout に出力するように構成されています。ここで、ログは Cloud Run Logging エージェントによって収集され、非同期で Cloud Logging に取り込まれます。main() 関数を変更して、構造化されたフォーマット ガイドラインに準拠した JSON スキーマを使用するように Go 標準の構造化ログを設定します。すべての return ステートメントは、終了前にエラーログを書き込むコードに置き換えられます。Handler() 関数は、Vertex AI API 呼び出しからレスポンスを受信したときに構造化ログを書き込むように計測されています。ログには、リクエストの動物パラメータ、モデルのプロンプトとレスポンスがキャプチャされます。

数秒後、Cloud Shell エディタによって変更が自動的に保存されます。

生成 AI アプリケーションのコードを Cloud Run にデプロイする

  1. ターミナル ウィンドウでコマンドを実行して、アプリケーションのソースコードを Cloud Run にデプロイします。
    gcloud run deploy codelab-o11y-service \
         --source="${HOME}/codelab-o11y/" \
         --region=us-central1 \
         --allow-unauthenticated
    
    コマンドで新しいリポジトリが作成されることを知らせるプロンプトが表示されます。[Enter] をクリックします。
    Deploying from source requires an Artifact Registry Docker repository to store built containers.
    A repository named [cloud-run-source-deploy] in region [us-central1] will be created.
    
    Do you want to continue (Y/n)?
    
    デプロイ プロセスには数分かかることがあります。デプロイ プロセスが完了すると、次のような出力が表示されます。
    Service [codelab-o11y-service] revision [codelab-o11y-service-00001-t2q] has been deployed and is serving 100 percent of traffic.
    Service URL: https://codelab-o11y-service-12345678901.us-central1.run.app
    
  2. 表示された Cloud Run サービスの URL を、ブラウザの別のタブまたはウィンドウにコピーします。または、ターミナルで次のコマンドを実行してサービス URL を出力し、表示された URL をクリックして Ctrl キーを押しながら URL を開きます。
    gcloud run services list \
         --format='value(URL)' \
         --filter='SERVICE:"codelab-o11y-service"'
    
    URL を開くと、500 エラーが発生するか、次のようなメッセージが表示されることがあります。
    Sorry, this is just a placeholder...
    
    サービスがデプロイを完了していないことを意味します。しばらく待ってからページを更新します。最後に、犬に関する面白い情報で始まるテキストが表示され、犬に関する 10 の面白い情報が記載されています。

アプリケーション ログを生成するには、サービス URL を開きます。?animal= パラメータの値を変更しながらページを更新して、異なる結果を取得します。
アプリケーション ログを表示する手順は次のとおりです。

  1. 次のボタンをクリックして、Cloud コンソールでログ エクスプローラ ページを開きます。

  2. 次のフィルタを [クエリ] ペイン(ログ エクスプローラ インターフェースの 2)に貼り付けます。
    LOG_ID("run.googleapis.com%2Fstdout") AND
    severity=DEBUG
    
  3. [クエリを実行] をクリックします。

クエリの結果には、プロンプトと Vertex AI レスポンス(安全性評価を含む)を含むログが表示されます。

9. 生成 AI によるインタラクションをカウントする

Cloud Run は、デプロイされたサービスをモニタリングするために使用できるマネージド指標を書き込みます。ユーザー管理のモニタリング指標を使用すると、データと指標の更新頻度をより細かく制御できます。このような指標を実装するには、データを収集して Cloud Monitoring に書き込むコードを記述する必要があります。OpenTelemetry SDK を使用して実装する方法については、次の(省略可)手順をご覧ください。

このステップでは、コードにユーザー指標を実装する代替方法であるログベースの指標について説明します。ログベースの指標を使用すると、アプリケーションが Cloud Logging に書き込むログエントリからモニタリング指標を生成できます。前の手順で実装したアプリケーション ログを使用して、タイプ カウンタのログベースの指標を定義します。この指標は、Vertex API への成功した呼び出しの数をカウントします。

  1. 前の手順で使用したログ エクスプローラのウィンドウを確認します。[クエリ] ペインで [アクション] プルダウン メニューを見つけてクリックし、開きます。メニューは以下のスクリーンショットをご覧ください。
    [操作] プルダウン メニューが表示されたクエリ結果ツールバー
  2. 開いたメニューで [指標を作成] を選択し、[ログベースの指標を作成] パネルを開きます。
  3. 次の手順に沿って、[ログベースの指標を作成] パネルで新しいカウンタ指標を構成します。
    1. [指標タイプ] を [カウンタ] に設定します。
    2. [詳細] セクションで次のフィールドを設定します。
      • ログ指標の名前: 名前を model_interaction_count に設定します。命名に関する制限事項が適用されます。詳細については、命名制限のトラブルシューティングをご覧ください。
      • 説明: 指標の説明を入力します。例: Number of log entries capturing successful call to model inference.
      • 単位: 空白のままにするか、数字「1」を挿入します。
    3. [フィルタの選択] セクションの値はそのままにします。[フィルタを作成] フィールドには、アプリケーション ログの表示に使用したフィルタと同じフィルタが設定されています。
    4. (省略可)動物ごとの呼び出し回数をカウントするラベルを追加します。注: このラベルを使用すると、指標のカーディナリティが大幅に増加する可能性があるため、本番環境での使用はおすすめしません。
      1. [ラベルを追加] をクリックします。
      2. [ラベル] セクションで次のフィールドを設定します。
        • ラベル名: 名前を animal に設定します。
        • 説明: ラベルの説明を入力します。例: Animal parameter
        • ラベルタイプ: STRING を選択します。
        • フィールド名: jsonPayload.animal と入力します。
        • 正規表現: 空白のままにします。
      3. [完了] をクリックします
    5. [指標を作成] をクリックして、メトリックを作成します。

[ログベースの指標] ページから、gcloud logging metrics create CLI コマンドまたは google_logging_metric Terraform リソースを使用してログベースの指標を作成することもできます。

指標データを生成するには、サービスの URL を開きます。開いたページを数回更新して、モデルを複数回呼び出します。前回と同様に、パラメータで別の動物を使用してみます。

PromQL クエリを入力して、ログベースの指標データを検索します。PromQL クエリを入力する手順は次のとおりです。

  1. 次のボタンをクリックして、Cloud コンソールで Metrics Explorer ページを開きます。

  2. クエリビルダー ペインのツールバーで、[< > MQL] または [< > PromQL] という名前のボタンを選択します。ボタンの場所については、下の画像をご覧ください。
    Metrics Explorer の MQL ボタンの場所
  3. [言語] 切り替えで [PromQL] が選択されていることを確認します。言語切り替えボタンは、クエリの書式設定を行うのと同じツールバーにあります。
  4. クエリ エディタにクエリを入力します。
    sum(rate(logging_googleapis_com:user_model_interaction_count{monitored_resource="cloud_run_revision"}[${__interval}]))
    
    PromQL の使用の詳細については、Cloud Monitoring の PromQL をご覧ください。
  5. [RUN QUERY] をクリックします。次のスクリーンショットに示すような折れ線グラフが表示されます。
    クエリされた指標を表示する

    [自動実行] の切り替えが有効になっている場合、[クエリを実行] ボタンは表示されません。

10. (省略可)モニタリングとトレースに Open Telemetry を使用する

前の手順で説明したように、OpenTelemetry(Otel)SDK を使用して指標を実装できます。マイクロサービス アーキテクチャで OTel を使用することをおすすめします。このステップでは、次のことを説明します。

  • アプリケーションのトレースとモニタリングをサポートするように OTel コンポーネントを初期化する
  • Cloud Run 環境のリソース メタデータで OTel 構成にデータを入力する
  • 自動トレース機能を使用して Flask アプリケーションを計測する
  • 成功したモデル呼び出しの数をモニタリングするカウンタ指標を実装する
  • トレースとアプリケーション ログを関連付ける

プロダクト レベルのサービスに推奨されるアーキテクチャは、OTel コレクタを使用して、1 つ以上のサービスのすべてのオブザーバビリティ データを収集して取り込むことです。このステップのコードでは、わかりやすくするためにコレクタを使用していません。代わりに、データを Google Cloud に直接書き込む OTel エクスポートを使用します。

トレースおよび指標モニタリング用の OTel コンポーネントを設定する

  1. ブラウザの [Cloud Shell] ウィンドウ(またはタブ)に戻ります。
  2. ターミナルで setup.go を再度開きます。
    cloudshell edit ~/codelab-o11y/setup.go
    
  3. コードを、OpenTelemetry のトレースと指標の収集を初期化するバージョンに置き換えます。コードを置き換えるには、ファイルの内容を削除し、次のコードをコピーしてエディタに貼り付けます。
    package main
    
    import (
        "context"
        "errors"
        "fmt"
        "net/http"
        "os"
    
        "log/slog"
    
        "go.opentelemetry.io/contrib/detectors/gcp"
        "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
        "go.opentelemetry.io/contrib/propagators/autoprop"
        "go.opentelemetry.io/otel"
        sdkmetric "go.opentelemetry.io/otel/sdk/metric"
        "go.opentelemetry.io/otel/sdk/resource"
        sdktrace "go.opentelemetry.io/otel/sdk/trace"
        semconv "go.opentelemetry.io/otel/semconv/v1.27.0"
        "go.opentelemetry.io/otel/trace"
    
        cloudmetric "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric"
        cloudtrace "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace"
    
        "cloud.google.com/go/compute/metadata"
    )
    
    var (
        projID string
    )
    
    func projectID(ctx context.Context) (string, error) {
        var projectID = os.Getenv("GOOGLE_CLOUD_PROJECT")
        if projectID == "" {
            return metadata.ProjectIDWithContext(ctx)
        }
        return projectID, nil
    }
    
    func setupLogging() {
        opts := &slog.HandlerOptions{
            Level: slog.LevelDebug,
            ReplaceAttr: func(group []string, a slog.Attr) slog.Attr {
                switch a.Key {
                case slog.LevelKey:
                    a.Key = "severity"
                    if level := a.Value.Any().(slog.Level); level == slog.LevelWarn {
                        a.Value = slog.StringValue("WARNING")
                    }
                case slog.MessageKey:
                    a.Key = "message"
                case slog.TimeKey:
                    a.Key = "timestamp"
                }
                return a
            },
        }
        jsonHandler := slog.NewJSONHandler(os.Stdout, opts)
        instrumentedHandler := handlerWithSpanContext(jsonHandler)
        slog.SetDefault(slog.New(instrumentedHandler))
    }
    
    type spanContextLogHandler struct {
        slog.Handler
    }
    
    func handlerWithSpanContext(handler slog.Handler) *spanContextLogHandler {
        return &spanContextLogHandler{Handler: handler}
    }
    
    func (t *spanContextLogHandler) Handle(ctx context.Context, record slog.Record) error {
        if s := trace.SpanContextFromContext(ctx); s.IsValid() {
            trace := fmt.Sprintf("projects/%s/traces/%s", projID, s.TraceID())
            record.AddAttrs(
                slog.Any("logging.googleapis.com/trace", trace),
            )
            record.AddAttrs(
                slog.Any("logging.googleapis.com/spanId", s.SpanID()),
            )
            record.AddAttrs(
                slog.Bool("logging.googleapis.com/trace_sampled", s.TraceFlags().IsSampled()),
            )
        }
        return t.Handler.Handle(ctx, record)
    }
    
    func setupTelemetry(ctx context.Context) (shutdown func(context.Context) error, err error) {
        var shutdownFuncs []func(context.Context) error
        shutdown = func(ctx context.Context) error {
            var err error
            for _, fn := range shutdownFuncs {
                err = errors.Join(err, fn(ctx))
            }
            shutdownFuncs = nil
            return err
        }
    
        projID, err = projectID(ctx)
        if err != nil {
            err = errors.Join(err, shutdown(ctx))
            return
        }
    
        res, err2 := resource.New(
            ctx,
            resource.WithDetectors(gcp.NewDetector()),
            resource.WithTelemetrySDK(),
            resource.WithAttributes(semconv.ServiceNameKey.String(os.Getenv("K_SERVICE"))),
        )
        if err2 != nil {
            err = errors.Join(err2, shutdown(ctx))
            return
        }
    
        otel.SetTextMapPropagator(autoprop.NewTextMapPropagator())
    
        texporter, err2 := cloudtrace.New(cloudtrace.WithProjectID(projID))
        if err2 != nil {
            err = errors.Join(err2, shutdown(ctx))
            return
        }
        tp := sdktrace.NewTracerProvider(
            sdktrace.WithSampler(sdktrace.AlwaysSample()),
            sdktrace.WithResource(res),
            sdktrace.WithBatcher(texporter))
        shutdownFuncs = append(shutdownFuncs, tp.Shutdown)
        otel.SetTracerProvider(tp)
    
        mexporter, err2 := cloudmetric.New(cloudmetric.WithProjectID(projID))
        if err2 != nil {
            err = errors.Join(err2, shutdown(ctx))
            return
        }
        mp := sdkmetric.NewMeterProvider(
            sdkmetric.WithReader(sdkmetric.NewPeriodicReader(mexporter)),
            sdkmetric.WithResource(res),
        )
        shutdownFuncs = append(shutdownFuncs, mp.Shutdown)
        otel.SetMeterProvider(mp)
    
        return shutdown, nil
    }
    
    func registerHttpHandler(route string, handleFn http.HandlerFunc) {
        instrumentedHandler := otelhttp.NewHandler(otelhttp.WithRouteTag(route, handleFn), route)
        http.Handle(route, instrumentedHandler)
    }
    
  4. ターミナルに戻り、次のコマンドを実行して go.mod ファイル内の Go モジュール定義を更新します。
    go mod tidy
    
  5. ターミナルに戻り、main.go を再度開きます。
    cloudshell edit ~/codelab-o11y/main.go
    
  6. 現在のコードを、HTTP トレース計測を行い、パフォーマンス指標を書き込むバージョンに置き換えます。コードを置き換えるには、ファイルの内容を削除し、次のコードをコピーしてエディタに貼り付けます。
    package main
    
    import (
        "context"
        "errors"
        "fmt"
        "net/http"
        "os"
    
        "encoding/json"
        "log/slog"
    
        "cloud.google.com/go/vertexai/genai"
    
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/attribute"
        "go.opentelemetry.io/otel/metric"
    )
    
    var model *genai.GenerativeModel
    var counter metric.Int64Counter
    
    const scopeName = "genai-o11y/go/workshop/example"
    
    func main() {
        ctx := context.Background()
        projectID, err := projectID(ctx)
        if err != nil {
            return
        }
    
        setupLogging()
        shutdown, err := setupTelemetry(ctx)
        if err != nil {
            slog.ErrorContext(ctx, "error setting up OpenTelemetry", slog.Any("error", err))
            os.Exit(1)
        }
        meter := otel.Meter(scopeName)
        counter, err = meter.Int64Counter("model_call_counter")
        if err != nil {
            slog.ErrorContext(ctx, "error setting up OpenTelemetry", slog.Any("error", err))
            os.Exit(1)
        }
    
        var client *genai.Client
        client, err = genai.NewClient(ctx, projectID, "us-central1")
        if err != nil {
            slog.ErrorContext(ctx, "Failed to marshal response to JSON", slog.Any("error", err))
            os.Exit(1)
        }
        defer client.Close()
        model = client.GenerativeModel("gemini-1.5-flash-001")
    
        registerHttpHandler("/", Handler)
    
        port := os.Getenv("PORT")
        if port == "" {
            port = "8080"
        }
    
        if err = errors.Join(http.ListenAndServe(":"+port, nil), shutdown(ctx)); err != nil {
            slog.ErrorContext(ctx, "Failed to start the server", slog.Any("error", err))
            os.Exit(1)
        }
    }
    
    func Handler(w http.ResponseWriter, r *http.Request) {
        animal := r.URL.Query().Get("animal")
        if animal == "" {
            animal = "dog"
        }
    
        prompt := fmt.Sprintf("Give me 10 fun facts about %s. Return the results as HTML without markdown backticks.", animal)
        resp, err := model.GenerateContent(r.Context(), genai.Text(prompt))
        if err != nil {
            w.WriteHeader(http.StatusTooManyRequests)
            return
        }
        jsonBytes, err := json.Marshal(resp)
        if err != nil {
            slog.ErrorContext(r.Context(), "Failed to marshal response to JSON", slog.Any("error", err))
        } else {
            slog.DebugContext(r.Context(), "content is generated", slog.String("animal", animal),
                slog.String("prompt", prompt), slog.String("response", string(jsonBytes)))
        }
        if len(resp.Candidates) > 0 && len(resp.Candidates[0].Content.Parts) > 0 {
            clabels := []attribute.KeyValue{attribute.Key("animal").String(animal)}
            counter.Add(r.Context(), 1, metric.WithAttributes(clabels...))
            htmlContent := resp.Candidates[0].Content.Parts[0]
            w.Header().Set("Content-Type", "text/html; charset=utf-8")
            fmt.Fprint(w, htmlContent)
        }
    }
    

これで、アプリケーションは OpenTelemetry SDK を使用して、トレースでコード実行を計測し、成功した実行回数を指標としてカウントする実装を行うようになりました。main() メソッドが変更され、Google Cloud Tracing と Monitoring に直接書き込むトレースと指標の OpenTelemetry エクスポータが設定されるようになりました。また、追加の構成を実行して、収集されたトレースおよび指標に Cloud Run 環境に関連するメタデータを入力します。Handler() 関数は、Vertex AI API 呼び出しが有効な結果を返すたびに指標カウンタをインクリメントするように更新されます。

数秒後、Cloud Shell エディタによって変更が自動的に保存されます。

生成 AI アプリケーションのコードを Cloud Run にデプロイする

  1. ターミナル ウィンドウでコマンドを実行して、アプリケーションのソースコードを Cloud Run にデプロイします。
    gcloud run deploy codelab-o11y-service \
         --source="${HOME}/codelab-o11y/" \
         --region=us-central1 \
         --allow-unauthenticated
    
    コマンドで新しいリポジトリが作成されることを知らせるプロンプトが表示されます。[Enter] をクリックします。
    Deploying from source requires an Artifact Registry Docker repository to store built containers.
    A repository named [cloud-run-source-deploy] in region [us-central1] will be created.
    
    Do you want to continue (Y/n)?
    
    デプロイ プロセスには数分かかることがあります。デプロイ プロセスが完了すると、次のような出力が表示されます。
    Service [codelab-o11y-service] revision [codelab-o11y-service-00001-t2q] has been deployed and is serving 100 percent of traffic.
    Service URL: https://codelab-o11y-service-12345678901.us-central1.run.app
    
  2. 表示された Cloud Run サービスの URL を、ブラウザの別のタブまたはウィンドウにコピーします。または、ターミナルで次のコマンドを実行してサービス URL を印刷し、表示された URL をクリックして Ctrl キーを押しながら URL を開きます。
    gcloud run services list \
         --format='value(URL)' \
         --filter='SERVICE:"codelab-o11y-service"'
    
    URL を開くと、500 エラーが発生するか、次のようなメッセージが表示されることがあります。
    Sorry, this is just a placeholder...
    
    サービスがデプロイを完了していないことを意味します。しばらく待ってからページを更新します。最後に、犬に関する面白い情報で始まるテキストが表示され、犬に関する 10 の面白い情報が記載されています。

テレメトリー データを生成するには、サービスの URL を開きます。?animal= パラメータの値を変更しながらページを更新して、異なる結果を取得します。

アプリケーション トレースを調べる

  1. 次のボタンをクリックして、Cloud コンソールで Trace エクスプローラ ページを開きます。

  2. 最新のトレースを選択します。次のスクリーンショットのような 5 ~ 6 つのスパンが表示されます。
    Trace エクスプローラでのアプリスパンの表示
  3. イベント ハンドラ(fun_facts メソッド)への呼び出しをトレースするスパンを探します。/ という名前の最後のスパンが対象です。
  4. [トレースの詳細] ペインで、[ログとイベント] を選択します。この特定のスパンに関連するアプリケーション ログが表示されます。相関は、トレース内とログ内のトレース ID とスパン ID を使用して検出されます。プロンプトと Vertex API のレスポンスを書き込んだアプリケーション ログが表示されます。

カウンタ指標を確認する

  1. 次のボタンをクリックして、Cloud コンソールで Metrics Explorer ページを開きます。

  2. クエリビルダー ペインのツールバーで、[< > MQL] または [< > PromQL] という名前のボタンを選択します。ボタンの場所については、下の画像をご覧ください。
    Metrics Explorer の MQL ボタンの場所
  3. [言語] 切り替えで [PromQL] が選択されていることを確認します。言語切り替えボタンは、クエリの書式設定を行うのと同じツールバーにあります。
  4. クエリ エディタにクエリを入力します。
    sum(rate(workload_googleapis_com:model_call_counter{monitored_resource="generic_task"}[${__interval}]))
    
  5. [クエリを実行] をクリックします。[自動実行] の切り替えが有効になっている場合、[クエリを実行] ボタンは表示されません。

11. (省略可)ログから難読化された機密情報

ステップ 10 では、アプリケーションが Gemini モデルとやり取りした際の情報がログに記録されました。この情報には、動物の名前、実際のプロンプト、モデルのレスポンスが含まれていました。この情報をログに保存することは安全ですが、他の多くのシナリオでは必要ありません。プロンプトには、ユーザーが保存を希望しない個人情報や機密情報が含まれている場合があります。この問題に対処するには、Cloud Logging に書き込まれる機密データを難読化します。コードの変更を最小限に抑えるには、次のソリューションをおすすめします。

  1. 受信したログエントリを保存する Pub/Sub トピックを作成する
  2. 取り込まれたログを PubSub トピックにリダイレクトするログシンクを作成します。
  3. 次の手順に沿って、Pub/Sub トピックにリダイレクトされたログを変更する Dataflow パイプラインを作成します。
    1. Pub/Sub トピックからログエントリを読み取る
    2. DLP 検査 API を使用して、エントリのペイロードに機密情報がないか検査する
    3. DLP の秘匿化方法のいずれかを使用して、ペイロード内の機密情報を秘匿化する
    4. 難読化されたログエントリを Cloud Logging に書き込む
  4. パイプラインをデプロイする

12. (省略可)クリーンアップ

Codelab で使用したリソースと API に対して課金されるリスクを回避するため、Lab の終了後にクリーンアップすることをおすすめします。課金を停止する最も簡単な方法は、Codelab 用に作成したプロジェクトを削除することです。

  1. プロジェクトを削除するには、ターミナルで delete project コマンドを実行します。
    PROJECT_ID=$(gcloud config get-value project)
    gcloud projects delete ${PROJECT_ID} --quiet
    
    Cloud プロジェクトを削除すると、そのプロジェクト内で使用されているすべてのリソースと API に対する課金が停止します。次のようなメッセージが表示されます。ここで PROJECT_ID はプロジェクト ID です。
    Deleted [https://cloudresourcemanager.googleapis.com/v1/projects/PROJECT_ID].
    
    You can undo this operation for a limited period by running the command below.
        $ gcloud projects undelete PROJECT_ID
    
    See https://cloud.google.com/resource-manager/docs/creating-managing-projects for information on shutting down projects.
    
  2. (省略可)エラーが発生した場合は、手順 5 でラボで使用したプロジェクト ID を確認します。それを最初の手順のコマンドに置き換えます。たとえば、プロジェクト ID が lab-example-project の場合、コマンドは次のようになります。
    gcloud projects delete lab-project-id-example --quiet
    

13. 完了

このラボでは、Gemini モデルを使用して予測を行う生成 AI アプリケーションを作成しました。必要なモニタリング機能とロギング機能をアプリケーションに実装しました。アプリケーションと変更をソースコードから Cloud Run にデプロイしました。次に、Google Cloud Observability プロダクトを使用してアプリケーションのパフォーマンスを追跡し、アプリケーションの信頼性を確保します。

本日ご利用いただいたプロダクトの改善につながるユーザー エクスペリエンス(UX)調査にご協力いただける場合は、こちらからご登録ください。

学習を継続するためのオプションは次のとおりです。