在 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 編輯器。如果系統顯示以下訊息,要求您授權 Cloud Shell 使用您的憑證呼叫 gcloud,請按一下「授權」繼續操作。
    點選授權 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 後,請按 NEnter,然後嘗試再次執行 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. 建立 Gen 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 編輯器會自動儲存程式碼。

將 Gen 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 服務網址複製到瀏覽器的其他分頁或視窗中。或者,您也可以在終端機中執行下列指令,列印服務網址,然後按住 Ctrl 鍵點選顯示的網址,開啟網址:
    gcloud run services list \
         --format='value(URL)' \
         --filter='SERVICE:"codelab-o11y-service"'
    
    開啟網址時,您可能會收到 500 錯誤或看到以下訊息:
    Sorry, this is just a placeholder...
    
    表示服務未完成部署。請稍候片刻,然後重新整理頁面。最後,您會看到開頭為「有趣的狗狗小知識」的文字,其中包含 10 個關於狗狗的有趣小知識。

請嘗試與應用程式互動,瞭解不同動物的趣味知識。如要執行這項操作,請將 animal 參數附加到網址,例如 ?animal=[ANIMAL],其中 [ANIMAL] 是動物名稱。舉例來說,附加 ?animal=cat 可取得 10 個關於貓咪的趣味小知識,附加 ?animal=sea turtle 則可取得 10 個關於海龜的趣味小知識。

7. 稽核 Vertex API 呼叫

稽核 Google API 呼叫可回答「誰在何時何地呼叫特定 API?」等問題。在排解應用程式問題、調查資源耗用情形或執行軟體鑑識分析時,稽核作業就非常重要。

稽核記錄可讓您追蹤管理和系統活動,以及記錄「資料讀取」和「資料寫入」API 作業的呼叫。如要稽核 Vertex AI 產生內容的要求,您必須在 Cloud 控制台中啟用「資料讀取」稽核記錄

  1. 按一下下方按鈕,在 Cloud 控制台中開啟「Audit Logs」(稽核記錄) 頁面

  2. 請確認頁面已選取您為本實驗室建立的專案。選取的專案會顯示在頁面左上角的漢堡選單中:
    Google Cloud 控制台專案下拉式選單
    如有需要,請從組合框選取正確的專案。
  3. 在「資料存取稽核記錄設定」表格中,在「Service」(服務) 欄中找到 Vertex AI API 服務,然後選取服務名稱左側的核取方塊,即可選取該服務。
    選取 Vertex AI API
  4. 在右側的資訊面板中,選取「資料讀取」稽核類型。
    檢查資料讀取記錄
  5. 按一下 [儲存]

如要產生稽核記錄,請開啟服務網址。變更 ?animal= 參數的值,然後重新整理頁面,即可取得不同的結果。

探索稽核記錄

  1. 按一下下方按鈕,在 Cloud 控制台中開啟「Logs Explorer」(記錄檔探索工具) 頁面:

  2. 將下列篩選器貼到「查詢」窗格中。
    LOG_ID("cloudaudit.googleapis.com%2Fdata_access") AND
    protoPayload.serviceName="aiplatform.googleapis.com"
    
    「查詢」窗格是位於 Logs Explorer 頁面頂端附近的編輯器:
    查詢稽核記錄
  3. 點選「執行查詢」
  4. 選取其中一個稽核記錄項目,然後展開欄位,查看記錄中擷取的資訊。
    您可以查看 Vertex API 呼叫的詳細資料,包括所用的方法和模型。您也可以查看叫用者的身分,以及呼叫的授權權限。

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() 函式,設定 Go 標準結構化記錄,以便使用符合結構化格式規範的 JSON 結構定義。所有 return 陳述式都會替換為會在結束前寫入錯誤記錄的程式碼。Handler() 函式會在收到 Vertex AI API 呼叫的回應時,記錄結構化記錄。記錄會擷取要求的動物參數,以及模型的提示和回應。

幾秒後,Cloud Shell 編輯器會自動儲存變更。

將 Gen 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 服務網址複製到瀏覽器的其他分頁或視窗中。或者,您也可以在終端機中執行下列指令,列印服務網址,然後按住 Ctrl 鍵點選顯示的網址,開啟網址:
    gcloud run services list \
         --format='value(URL)' \
         --filter='SERVICE:"codelab-o11y-service"'
    
    開啟網址時,您可能會收到 500 錯誤或看到以下訊息:
    Sorry, this is just a placeholder...
    
    表示服務未完成部署。請稍候片刻,然後重新整理頁面。最後,您會看到開頭為「有趣的狗狗小知識」的文字,其中包含 10 個關於狗狗的有趣小知識。

如要產生應用程式記錄,請開啟服務網址。變更 ?animal= 參數的值,然後重新整理頁面,即可取得不同的結果。
如要查看應用程式記錄,請按照下列步驟操作:

  1. 按一下下方按鈕,在 Cloud 控制台中開啟「Logs Explorer」(記錄檔探索工具) 頁面:

  2. 將下列篩選器貼到「查詢」窗格 (記錄檔探索工具介面中的 #2):
    LOG_ID("run.googleapis.com%2Fstdout") AND
    severity=DEBUG
    
  3. 點選「執行查詢」

查詢結果會顯示含有提示和 Vertex AI 回應的記錄,包括安全評分

9. 計算與生成式 AI 的互動次數

Cloud Run 會寫入受管理指標,可用於監控已部署的服務。使用者管理的監控指標可讓您進一步控管資料和指標更新頻率。如要實作這類指標,您必須編寫程式碼來收集資料,並將資料寫入 Cloud Monitoring。請參閱下一個 (選用) 步驟,瞭解如何使用 OpenTelemetry SDK 導入這項功能。

這個步驟會說明在程式碼中導入使用者指標的替代做法,也就是以記錄為準的指標。您可以使用記錄指標,根據應用程式寫入 Cloud Logging 的記錄項目產生監控指標。我們將使用先前步驟中實作的應用程式記錄,定義 type counter 的記錄式指標。指標會計算成功呼叫 Vertex API 的次數。

  1. 請查看我們在上一個步驟中使用的「Logs Explorer」視窗。在「查詢」窗格下方找到「動作」下拉式選單,然後點選該選單來開啟。請參考下方螢幕截圖,找出選單:
    查詢結果工具列,內含「動作」下拉式選單
  2. 在開啟的選單中選取「Create metric」,即可開啟「Create log-based metric」面板。
  3. 請按照下列步驟,在「Create log-based metric」面板中設定新的計數器指標:
    1. 設定「指標類型」:選取「計數器」
    2. 在「詳細資料」部分設定下列欄位:
      • 記錄指標名稱:將名稱設為 model_interaction_count。存在一些命名限制;詳情請參閱疑難排解一文。
      • 說明:輸入指標說明。例如:Number of log entries capturing successful call to model inference.
      • 單位:請將這個欄位留空或輸入數字 1
    3. 保留「Filter selection」部分的值。請注意,「Build filter」欄位與我們用來查看應用程式記錄的篩選器相同。
    4. (選用) 新增標籤,以便計算每種動物的呼叫次數。注意:這個標籤可能會大幅增加指標的基數,因此不建議在正式環境中使用:
      1. 按一下 [Add label] (新增標籤)
      2. 在「標籤」部分設定下列欄位:
        • 標籤名稱:將名稱設為 animal
        • 說明:輸入標籤的說明。例如 Animal parameter
        • 標籤類型:選取 STRING
        • 欄位名稱:輸入 jsonPayload.animal
        • 規則運算式:留空。
      3. 然後按一下 [完成]
    5. 點選「建立指標」建立指標。

您也可以使用 gcloud logging metrics create CLI 指令google_logging_metric Terraform 資源,在「記錄指標」頁面建立記錄指標。

如要產生指標資料,請開啟服務網址。重新整理已開啟的頁面多次,以便多次呼叫模型。如同先前所述,請嘗試在參數中使用其他動物。

輸入 PromQL 查詢,以便搜尋記錄指標資料。如要輸入 PromQL 查詢,請按照下列步驟操作:

  1. 按一下下方按鈕,在 Cloud 控制台中開啟「Metrics Explorer」頁面:

  2. 在查詢建構工具窗格中的工具列中,選取名稱為 < > MQL< > PromQL 的按鈕。請參閱下圖,瞭解按鈕的位置。
    Metrics Explorer 中 MQL 按鈕的位置
  3. 確認「Language」切換按鈕中已選取「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 收集器收集及攝入一或多項服務的所有觀測資料。為簡化程序,本步驟中的程式碼不會使用收集器。而是使用 OTel 匯出功能,直接將資料寫入 Google Cloud。

設定 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 環境相關的中繼資料。每次 Vertex AI API 呼叫傳回有效結果時,系統都會更新 Handler() 函式,以便增加指標計數器。

幾秒後,Cloud Shell 編輯器會自動儲存變更。

將 Gen 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 服務網址複製到瀏覽器的其他分頁或視窗中。或者,您也可以在終端機中執行下列指令,列印服務網址,然後按住 Ctrl 鍵點選顯示的網址,開啟網址:
    gcloud run services list \
         --format='value(URL)' \
         --filter='SERVICE:"codelab-o11y-service"'
    
    開啟網址時,您可能會收到 500 錯誤或看到以下訊息:
    Sorry, this is just a placeholder...
    
    表示服務未完成部署。請稍候片刻,然後重新整理頁面。最後,您會看到開頭為「有趣的狗狗小知識」的文字,其中包含 10 個關於狗狗的有趣小知識。

如要產生遙測資料,請開啟服務網址。變更 ?animal= 參數的值,然後重新整理頁面,即可取得不同的結果。

探索應用程式追蹤記錄

  1. 按一下下方按鈕,在 Cloud 控制台中開啟「Trace explorer」(追蹤記錄探索工具) 頁面:

  2. 選取最近的追蹤記錄。您應該會看到 5 或 6 個 span,如下方螢幕截圖所示。
    在 Trace 探索器中查看應用程式時距
  3. 找出追蹤事件處理常式 (fun_facts 方法) 呼叫的跨度。這個 span 的名稱會是 /
  4. 在「Trace details」窗格中,選取「Logs & events」。您會看到與這個特定時距相關聯的應用程式記錄檔。系統會使用追蹤記錄和記錄中的追蹤記錄和時距 ID 偵測關聯性。您應該會看到寫入提示和 Vertex API 回應的應用程式記錄。

探索計數器指標

  1. 按一下下方按鈕,在 Cloud 控制台中開啟「Metrics Explorer」頁面:

  2. 在查詢建構工具窗格中的工具列中,選取名稱為 < > MQL< > PromQL 的按鈕。請參閱下圖,瞭解按鈕的位置。
    Metrics Explorer 中 MQL 按鈕的位置
  3. 確認「Language」切換按鈕中已選取「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. 建立記錄接收器,將擷取的記錄重新導向至 Pub/Sub 主題。
  3. 建立 Dataflow 管道,按照下列步驟修改重新導向至 Pub/Sub 主題的記錄:
    1. 讀取 Pub/Sub 主題中的記錄項目
    2. 使用 DLP 檢查 API 檢查項目的酬載內容是否含有機密資訊
    3. 使用其中一種 DLP 遮蓋方法遮蓋酬載中的機密資訊
    4. 將經過模糊處理的記錄項目寫入 Cloud Logging
  4. 部署管道

12. (選用) 清除

為避免產生費用,請在完成實驗室後清除實驗室中使用的資源和 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 模型進行預測的 Gen AI 應用程式。並透過必要的監控和記錄功能對應用程式進行檢測。您已將應用程式和原始碼變更部署至 Cloud Run。接著,您可以使用 Google Cloud Observability 產品追蹤應用程式的效能,確保應用程式可靠。

如有意參與使用者體驗 (UX) 研究,協助改善您目前使用的產品,請按這裡註冊。

以下提供一些繼續學習的選項: