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 の [マイ プロジェクト] で課金が有効になっていることを確認します。
    • 新しいプロジェクトの 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 の認証情報をリクエストしていることを示す次のメッセージが表示されたら、[Authorize] をクリックして続行します。
    Cloud Shell を承認する
    正常に実行されると、次のメッセージが表示されます。
    Updated property [core/project].
    
    WARNING が表示され、Do you want to continue (Y/N)? と表示された場合は、プロジェクト ID が正しく入力されていない可能性があります。N を押し、Enter を押します。正しいプロジェクト ID が見つかったら、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 用の Metadata ライブラリをインストールして、現在のプロジェクト 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=[ANIMAL] のように、animal パラメータを URL に追加します。ここで、[ANIMAL] は動物の名前です。たとえば、?animal=cat を追加すると猫に関する 10 個の豆知識が、?animal=sea turtle を追加するとウミガメに関する 10 個の豆知識が返されます。

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

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

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

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

  2. このラボ用に作成したプロジェクトがページで選択されていることを確認します。選択したプロジェクトは、ハンバーガー メニューの右側のページ左上に表示されます。
    Google Cloud Console のプロジェクト プルダウン
    必要に応じて、コンボボックスから正しいプロジェクトを選択します。
  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 ロギング エージェントによってログが収集され、Cloud Logging に非同期で取り込まれます。main() 関数が変更され、構造化された形式のガイドラインに沿った JSON スキーマを使用するように Go 標準の構造化ログが設定されます。すべての return ステートメントが、終了前にエラーログを書き込むコードに置き換えられます。Handler() 関数は、Vertex AI API 呼び出しからレスポンスを受信したときに構造化ログを書き込むように計測されます。ログには、リクエストの animal パラメータとモデルのプロンプトとレスポンスが記録されます。

数秒後、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. [Queries] エディタにクエリを入力します。
    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() メソッドが変更され、トレースと指標の OpenTelemetry エクスポータが設定されて、Google Cloud Tracing と Monitoring に直接書き込むようになります。また、収集されたトレースと指標に 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. [Queries] エディタにクエリを入力します。
    sum(rate(workload_googleapis_com:model_call_counter{monitored_resource="generic_task"}[${__interval}]))
    
  5. [クエリを実行] をクリックします。[自動実行] の切り替えが有効になっている場合、[クエリを実行] ボタンは表示されません。

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

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

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

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

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

  1. プロジェクトを削除するには、ターミナルでプロジェクト削除コマンドを実行します。
    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)調査にご協力いただける場合は、こちらからご登録ください

学習を継続するためのオプションをいくつかご紹介します。