1. 概览
与任何其他应用一样,生成式 AI 应用也需要可观测性。生成式 AI 是否需要特殊的可观测性技术?
在本实验中,您将创建一个简单的生成式 AI 应用。将其部署到 Cloud Run。并使用 Google Cloud 可观测性服务和产品为其添加基本监控和日志记录功能。
学习内容
- 使用 Cloud Shell Editor 编写一个使用 Vertex AI 的应用
- 将应用代码存储在 GitHub 中
- 使用 gcloud CLI 将应用的源代码部署到 Cloud Run
- 向生成式 AI 应用添加监控和日志记录功能
- 使用基于日志的指标
- 使用 Open Telemetry SDK 实现日志记录和监控
- 深入了解负责任 AI 数据处理
2. 前提条件
如果您还没有 Google 账号,则必须创建一个新账号。
3. 项目设置
- 使用您的 Google 账号登录 Google Cloud 控制台。
- 创建新项目或选择重复使用现有项目。记下您刚刚创建或选择的项目的项目 ID。
- 为项目启用结算功能。
- 完成此实验的计费费用应低于 5 美元。
- 您可以按照本实验最后的步骤删除资源,以免产生更多费用。
- 新用户符合参与 300 美元免费试用计划的条件。
- 在 Cloud Billing 的我负责的项目中确认已启用结算功能
- 如果新项目的
Billing account
列显示Billing is disabled
:- 点击
Actions
列中的三点状图标 - 点击更改结算信息
- 选择要使用的结算账号
- 点击
- 如果您参加的是线下活动,该账号的名称可能为 Google Cloud Platform 试用结算账号
- 如果新项目的
4. 准备 Cloud Shell Editor
- 前往 Cloud Shell Editor。如果系统提示您授权 Cloud Shell 使用您的凭据调用 gcloud,请点击授权继续。
- 打开终端窗口
- 点击汉堡式菜单
- 点击终端
- 点击 New Terminal
- 点击汉堡式菜单
- 在终端中,配置您的项目 ID:
将gcloud config set project [PROJECT_ID]
[PROJECT_ID]
替换为您的项目 ID。例如,如果您的项目 ID 为lab-example-project
,则命令将为: 如果系统提示您 gcloud 正在请求您的 GCPI API 凭据,请点击授权以继续。gcloud config set project lab-project-id-example
执行成功后,您应该会看到以下消息: 如果您看到Updated property [core/project].
WARNING
并收到Do you want to continue (Y/N)?
询问,则可能输入的项目 ID 有误。找到正确的项目 ID 后,请按N
和Enter
,然后尝试再次运行gcloud config set project
命令。 - (可选)如果您在查找项目 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 个有趣事实。请按以下步骤创建应用代码。
- 在终端中,创建
codelab-o11y
目录:mkdir ~/codelab-o11y
- 将当前目录更改为
codelab-o11y
:cd ~/codelab-o11y
- 初始化 Go 模块:
go mod init codelab
- 安装 Go 版 Vertex AI SDK:
go get cloud.google.com/go/vertexai/genai
- 安装适用于 Go 的元数据库以获取当前项目 ID:
go get cloud.google.com/go/compute/metadata
- 创建一个
setup.go
文件,并在 Cloud Shell Editor 中打开该文件: 它将用于托管初始化代码。编辑器窗口中会显示一个名为cloudshell edit setup.go
setup.go
的新空文件。 - 复制以下代码并将其粘贴到打开的
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 }
- 返回终端窗口,然后运行以下命令以在 Cloud Shell Editor 中创建并打开
main.go
文件: 现在,终端上方的编辑器窗口中应该会显示一个空文件。屏幕将如下所示:cloudshell edit main.go
- 复制以下代码并将其粘贴到打开的
main.go
文件中: 几秒钟后,Cloud Shell 编辑器会自动保存您的代码。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) } }
将生成式 AI 应用的代码部署到 Cloud Run
- 在终端窗口中,运行命令以将应用的源代码部署到 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
- 将显示的 Cloud Run 服务网址复制到浏览器中的单独标签页或窗口中。或者,您也可以在终端中运行以下命令以输出服务网址,然后按住 Ctrl 键点击显示的网址以打开该网址:
打开网址时,您可能会收到 500 错误或看到以下消息:gcloud run services list \ --format='value(URL)' \ --filter='SERVICE:"codelab-o11y-service"'
这表示服务未完成部署。请稍等片刻,然后刷新页面。最后,您会看到以 Dog Fun Facts(狗狗趣事)开头的文字,其中包含 10 条与狗狗有关的趣事。Sorry, this is just a placeholder...
尝试与应用互动,了解各种动物的趣味知识。为此,请将 animal
参数附加到网址中,例如 ?animal=[ANIMAL]
,其中 [ANIMAL]
是动物名称。例如,附加 ?animal=cat
可获取 10 条关于猫的趣事,附加 ?animal=sea turtle
可获取 10 条关于海龟的趣事。
7. 审核您的 Vertex API 调用
通过审核 Google API 调用,您可以解答“哪些用户何时在何处调用了特定 API”等问题。在排查应用问题、调查资源消耗或执行软件取证分析时,请务必进行审核。
借助审核日志,您可以跟踪管理员活动和系统活动,以及记录对“数据读取”和“数据写入”API 操作的调用。如需审核生成内容的 Vertex AI 请求,您必须在 Cloud 控制台中启用“数据读取”审核日志。
- 点击下面的按钮,在 Cloud 控制台中打开“审核日志”页面
- 确保该页面已选择您为本实验创建的项目。所选项目显示在页面左上角三线状菜单的右侧:
如有必要,请从下拉菜单中选择正确的项目。 - 在数据访问审核日志配置表格中,找到“服务”列中的
Vertex AI API
服务,然后选中服务名称左侧的复选框以选择该服务。 - 在右侧的信息面板中,选择“数据读取”审核类型。
- 点击保存。
如需生成审核日志,请打开相应服务的网址。刷新页面,同时更改 ?animal=
参数的值,以获取不同的结果。
浏览审核日志
8. 记录与生成式 AI 的互动
您在审核日志中找不到 API 请求参数或响应数据。不过,这些信息对于排查应用和工作流分析问题可能很重要。在此步骤中,我们将通过添加应用日志记录来填补这一空白。日志记录使用标准 Go log/slog
软件包来写入结构化日志。log/slog
软件包不知道如何将日志写入 Google Cloud。它支持写入标准输出。不过,Cloud Run 提供捕获输出到标准输出的信息并自动将其提取到 Cloud Logging 的功能。为了正确捕获结构化日志,应相应地设置输出日志的格式。请按照以下说明向 Go 应用添加结构化日志记录功能。
- 返回浏览器中的“Cloud Shell”窗口(或标签页)。
- 在终端中,重新打开
setup.go
:cloudshell edit ~/codelab-o11y/setup.go
- 将代码替换为用于设置日志记录的版本。如需替换代码,请删除文件中的内容,然后复制以下代码并将其粘贴到编辑器中:
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)) }
- 返回终端,然后重新打开
main.go
:cloudshell edit ~/codelab-o11y/main.go
- 将应用代码替换为用于记录与模型互动的版本。如需替换代码,请删除文件的内容,然后复制以下代码并将其粘贴到编辑器中:
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 编辑器会自动保存您的更改。
将生成式 AI 应用的代码部署到 Cloud Run
- 在终端窗口中,运行命令以将应用的源代码部署到 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
- 将显示的 Cloud Run 服务网址复制到浏览器中的单独标签页或窗口中。或者,您也可以在终端中运行以下命令以输出服务网址,然后按住 Ctrl 键点击显示的网址以打开该网址:
打开网址时,您可能会收到 500 错误或看到以下消息:gcloud run services list \ --format='value(URL)' \ --filter='SERVICE:"codelab-o11y-service"'
这表示服务未完成部署。请稍等片刻,然后刷新页面。最后,您会看到以 Dog Fun Facts(狗狗趣事)开头的文字,其中包含 10 条与狗狗有关的趣事。Sorry, this is just a placeholder...
如需生成应用日志,请打开服务网址。刷新页面,同时更改 ?animal=
参数的值,以获取不同的结果。
如需查看应用日志,请执行以下操作:
- 点击下面的按钮,在 Cloud 控制台中打开“日志浏览器”页面:
- 将以下过滤条件粘贴到“查询”窗格(Logs Explorer 界面中的 2)中:
LOG_ID("run.googleapis.com%2Fstdout") AND severity=DEBUG
- 点击运行查询。
查询结果会显示包含提示和 Vertex AI 回答(包括安全评分)的日志。
9. 统计与生成式 AI 的互动次数
Cloud Run 会写入可用于监控已部署服务的受管式指标。用户管理的监控指标可让您更好地控制数据和指标更新频率。如需实现此类指标,您需要编写用于收集数据并将其写入 Cloud Monitoring 的代码。如需了解如何使用 OpenTelemetry SDK 实现此功能,请参阅下一步(可选)。
此步骤介绍了在代码中实现用户指标的替代方案:基于日志的指标。借助基于日志的指标,您可以根据应用写入 Cloud Logging 的日志条目生成监控指标。我们将使用在上一步中实现的应用日志来定义计数器类型的基于日志的指标。该指标将统计对 Vertex API 的成功调用次数。
- 查看我们在上一步中使用过的 Logs Explorer 窗口。在“查询”窗格下,找到操作下拉菜单,然后点击该菜单以将其打开。请参阅以下屏幕截图,找到该菜单:
- 在打开的菜单中,选择创建指标以打开创建基于日志的指标面板。
- 如需在创建基于日志的指标面板中配置新的计数器指标,请按以下步骤操作:
- 设置指标类型:选择计数器。
- 在详细信息部分中设置以下字段:
- 日志指标名称:将名称设置为
model_interaction_count
。您需遵循一些命名限制;如需了解详情,请参阅命名限制 问题排查。 - 说明:输入指标的说明。例如
Number of log entries capturing successful call to model inference.
- 单位:请将此字段留空或插入数字
1
。
- 日志指标名称:将名称设置为
- 在过滤器选择部分中,保留相应值。请注意,build 过滤条件字段与我们用于查看应用日志的过滤条件相同。
- (可选)添加一个标签,用于统计每只动物的呼叫次数。注意:此标签可能会大幅增加指标的基数,不建议在生产环境中使用:
- 点击添加标签。
- 在标签部分中设置以下字段:
- 标签名称:将名称设置为
animal
。 - 说明:输入标签的说明。例如
Animal parameter
。 - 标签类型:选择
STRING
。 - 字段名称:类型
jsonPayload.animal
。 - 正则表达式:留空。
- 标签名称:将名称设置为
- 点击完成。
- 点击创建指标,以创建指标。
您还可以使用 gcloud logging metrics create
CLI 命令或 google_logging_metric
Terraform 资源,从基于日志的指标页面创建基于日志的指标。
如需生成指标数据,请打开服务网址。刷新打开的页面几次,以多次调用模型。与之前一样,尝试在参数中使用不同的动物。
输入 PromQL 查询以搜索基于日志的指标数据。如需输入 PromQL 查询,请执行以下操作:
- 点击下面的按钮,在 Cloud 控制台中打开 Metrics Explorer 页面:
- 在查询构建器窗格的工具栏中,选择名称为 < > MQL 或 < > PromQL 的按钮。请参阅下图,了解该按钮的位置。
- 验证已在语言切换开关中选择 PromQL。语言切换开关位于同一工具栏中,用于设置查询的格式。
- 在查询编辑器中输入查询:
如需详细了解如何使用 PromQL,请参阅 Cloud Monitoring 中的 PromQL。sum(rate(logging_googleapis_com:user_model_interaction_count{monitored_resource="cloud_run_revision"}[${__interval}]))
- 点击运行查询。您会看到一个类似于以下屏幕截图的折线图:
请注意,启用自动运行切换开关后,系统不会显示运行查询按钮。
10. (可选)使用 OpenTelemetry 进行监控和跟踪
如上一步所述,您可以使用 OpenTelemetry (Otel) SDK 实现指标。建议在微服务架构中使用 OTel。此步骤介绍了以下内容:
- 初始化 OTel 组件以支持应用跟踪和监控
- 使用 Cloud Run 环境的资源元数据填充 OTel 配置
- 使用自动跟踪功能对 Flask 应用进行插桩
- 实现计数器指标以监控成功的模型调用次数
- 将跟踪记录与应用日志相关联
对于产品级服务,建议的架构是使用 OTel 收集器收集和提取一个或多个服务的所有可观测性数据。为简单起见,此步骤中的代码不使用收集器。而是使用会将数据直接写入 Google Cloud 的 Otel 导出功能。
设置 OTel 组件以进行跟踪和指标监控
- 返回浏览器中的“Cloud Shell”窗口(或标签页)。
- 在终端中,重新打开
setup.go
:cloudshell edit ~/codelab-o11y/setup.go
- 将该代码替换为用于初始化 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) }
- 返回终端,然后运行以下命令以更新
go.mod
文件中的 Go 模块定义:go mod tidy
- 返回终端,然后重新打开
main.go
:cloudshell edit ~/codelab-o11y/main.go
- 将当前代码替换为用于插桩 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 编辑器会自动保存您的更改。
将生成式 AI 应用的代码部署到 Cloud Run
- 在终端窗口中,运行命令以将应用的源代码部署到 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
- 将显示的 Cloud Run 服务网址复制到浏览器中的单独标签页或窗口中。或者,您也可以在终端中运行以下命令以输出服务网址,然后按住 Ctrl 键点击所显示的网址以打开该网址:
打开网址时,您可能会收到 500 错误或看到以下消息:gcloud run services list \ --format='value(URL)' \ --filter='SERVICE:"codelab-o11y-service"'
这表示服务未完成部署。请稍等片刻,然后刷新页面。最后,您会看到以 Dog Fun Facts(狗狗趣事)开头的文字,其中包含 10 条与狗狗有关的趣事。Sorry, this is just a placeholder...
如需生成遥测数据,请打开服务网址。刷新页面,同时更改 ?animal=
参数的值,以获取不同的结果。
探索应用轨迹
- 点击下面的按钮,在 Cloud 控制台中打开 Trace 探索器页面:
- 选择最近的轨迹之一。您应该会看到 5 到 6 个 span,如下面的屏幕截图所示。
- 找到用于跟踪对事件处理脚本(
fun_facts
方法)的调用的 span。它将是最后一个名称为/
的 span。 - 在跟踪记录详情窗格中,选择日志和事件。您会看到与此特定 span 相关联的应用日志。系统会使用跟踪记录和日志中的跟踪记录 ID 和 Span ID 来检测关联。您应该会看到写入问题的应用日志和 Vertex API 的响应。
探索计数器指标
- 点击下面的按钮,在 Cloud 控制台中打开 Metrics Explorer 页面:
- 在查询构建器窗格的工具栏中,选择名称为 < > MQL 或 < > PromQL 的按钮。请参阅下图,了解该按钮的位置。
- 验证已在语言切换开关中选择 PromQL。语言切换开关位于同一工具栏中,用于设置查询的格式。
- 在查询编辑器中输入查询:
sum(rate(workload_googleapis_com:model_call_counter{monitored_resource="generic_task"}[${__interval}]))
- 点击运行查询。启用自动运行切换开关后,系统不会显示运行查询按钮。
11. (可选)日志中经过混淆处理的敏感信息
在第 10 步中,我们记录了应用与 Gemini 模型互动的信息。这些信息包括动物的名称、实际问题和模型的回答。虽然将此类信息存储在日志中应该是安全的,但对于许多其他场景,这并不一定正确。提示中可能包含用户不希望存储的某些个人信息或其他敏感信息。为解决此问题,您可以混淆写入 Cloud Logging 的敏感数据。为最大限度地减少代码修改,建议采用以下解决方案。
- 创建一个 Pub/Sub 主题以存储传入的日志条目
- 创建一个日志接收器,将提取的日志重定向到 Pub/Sub 主题。
- 按照以下步骤创建一个 Dataflow 流水线,用于修改重定向到 Pub/Sub 主题的日志:
- 从 Pub/Sub 主题读取日志条目
- 使用 DLP 检查 API 检查条目的载荷是否包含敏感信息
- 使用某种 DLP 隐去方法隐去载荷中的敏感信息
- 将经过混淆处理的日志条目写入 Cloud Logging
- 部署流水线。
12. (可选)清理
为避免因本 Codelab 中使用的资源和 API 而产生费用,建议您在完成实验后进行清理。若要避免产生费用,最简单的方法是删除您为此 Codelab 创建的项目。
- 如需删除项目,请在终端中运行删除项目命令:
删除 Cloud 项目后,系统会停止对该项目中使用的所有资源和 API 计费。您应该会看到以下消息,其中PROJECT_ID=$(gcloud config get-value project) gcloud projects delete ${PROJECT_ID} --quiet
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.
- (可选)如果您收到错误消息,请参阅第 5 步,找到您在实验期间使用的项目 ID。将其替换为第一个说明中的命令。例如,如果您的项目 ID 为
lab-example-project
,则命令将为:gcloud projects delete lab-project-id-example --quiet
13. 恭喜
在本实验中,您创建了一个使用 Gemini 模型进行预测的 Gen AI 应用。并为应用添加了必要的监控和日志记录功能。您已将应用和源代码更改部署到 Cloud Run。然后,您可以使用 Google Cloud Observability 产品跟踪应用的性能,从而确保应用的可靠性。
如果您有兴趣参与用户体验 (UX) 调研,以便改进您目前使用的 Google 产品,请点击此处报名。
以下是继续学习的几种方法:
- Codelab:如何在 Cloud Run 上部署由 Gemini 提供支持的聊天应用
- Codelab:如何将 Gemini 函数调用与 Cloud Run 搭配使用
- 如何使用 Cloud Run 作业 Video Intelligence API 逐场景处理视频
- 点播研讨会 Google Kubernetes Engine 新手入门
- 详细了解如何使用应用日志配置计数器和分布指标
- 使用 OpenTelemetry Sidecar 写入 OTLP 指标
- 有关在 Google Cloud 中使用 OpenTelemetry 的参考文档