适用于 Java 中的生成式 AI 应用的实用可观测性技术

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. 项目设置

  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 Editor

  1. 前往 Cloud Shell Editor。如果系统提示您授权 Cloud Shell 使用您的凭据调用 gcloud,请点击授权继续。
    点击以授权 Cloud Shell
  2. 打开终端窗口
    1. 点击汉堡式菜单 汉堡式三线图标
    2. 点击终端
    3. 点击 New Terminal
      在 Cloud Shell Editor 中打开新终端
  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. 创建生成式 AI 应用

在此步骤中,您将编写基于请求的简单应用的代码,该应用使用 Gemini 模型显示您选择的动物的 10 个有趣事实。请按以下步骤创建应用代码。

  1. 在终端中,创建 codelab-o11y 目录:
    mkdir "${HOME}/codelab-o11y"
    
  2. 将当前目录更改为 codelab-o11y
    cd "${HOME}/codelab-o11y"
    
  3. 使用 Spring 框架起始代码下载 Java 应用的引导代码:
    curl https://start.spring.io/starter.zip \
        -d dependencies=web \
        -d javaVersion=17 \
        -d type=maven-project \
        -d bootVersion=3.4.1 -o java-starter.zip
    
  4. 将引导代码解压缩到当前文件夹:
    unzip java-starter.zip
    
  5. 然后从文件夹中移除归档文件:
    rm java-starter.zip
    
  6. 创建 project.toml 文件,以定义在将代码部署到 Cloud Run 时要使用的 Java 运行时版本:
    cat > "${HOME}/codelab-o11y/project.toml" << EOF
    [[build.env]]
        name = "GOOGLE_RUNTIME_VERSION"
        value = "17"
    EOF
    
  7. 将 Google Cloud SDK 依赖项添加到 pom.xml 文件中:
    1. 添加 Google Cloud Core 软件包:
      sed -i 's/<dependencies>/<dependencies>\
      \
              <dependency>\
                  <groupId>com.google.cloud<\/groupId>\
                  <artifactId>google-cloud-core<\/artifactId>\
                  <version>2.49.1<\/version>\
              <\/dependency>\
              /g' "${HOME}/codelab-o11y/pom.xml"
      
    2. 添加 Google Cloud Vertex AI 软件包:
      sed -i 's/<dependencies>/<dependencies>\
      \
              <dependency>\
                  <groupId>com.google.cloud<\/groupId>\
                  <artifactId>google-cloud-vertexai<\/artifactId>\
                  <version>1.16.0<\/version>\
              <\/dependency>\
              /g' "${HOME}/codelab-o11y/pom.xml"
      
  8. 在 Cloud Shell Editor 中打开 DemoApplication.java 文件:
    cloudshell edit "${HOME}/codelab-o11y/src/main/java/com/example/demo/DemoApplication.java"
    
    现在,终端上方的编辑器窗口中应该会显示文件 DemoApplication.java 的框架化源代码。文件的源代码将如下所示:
    package com.example.demo;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class DemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    }
    
  9. 将编辑器中的代码替换为下方所示的版本。如需替换代码,请删除文件的内容,然后将以下代码复制到编辑器中:
    package com.example.demo;
    
    import java.io.IOException;
    import java.util.Collections;
    
    import javax.annotation.PostConstruct;
    import javax.annotation.PreDestroy;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.google.cloud.ServiceOptions;
    import com.google.cloud.vertexai.VertexAI;
    import com.google.cloud.vertexai.api.GenerateContentResponse;
    import com.google.cloud.vertexai.generativeai.GenerativeModel;
    import com.google.cloud.vertexai.generativeai.ResponseHandler;
    
    @SpringBootApplication
    public class DemoApplication {
    
        public static void main(String[] args) {
            String port = System.getenv().getOrDefault("PORT", "8080");
            SpringApplication app = new SpringApplication(DemoApplication.class);
            app.setDefaultProperties(Collections.singletonMap("server.port", port));
            app.run(args);
        }
    }
    
    @RestController
    class HelloController {
        private final String projectId = ServiceOptions.getDefaultProjectId();
        private VertexAI vertexAI;
        private GenerativeModel model;
    
        @PostConstruct
        public void init() {
            vertexAI = new VertexAI(projectId, "us-central1");
            model = new GenerativeModel("gemini-1.5-flash", vertexAI);
        }
    
        @PreDestroy
        public void destroy() {
            vertexAI.close();
        }
    
        @GetMapping("/")
        public String getFacts(@RequestParam(defaultValue = "dog") String animal) throws IOException {
            String prompt = "Give me 10 fun facts about " + animal + ". Return this as html without backticks.";
            GenerateContentResponse response = model.generateContent(prompt);
            return ResponseHandler.getText(response);
        }
    }
    
    几秒钟后,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 服务网址复制到浏览器中的单独标签页或窗口中。或者,您也可以在终端中运行以下命令以输出服务网址,然后按住 Ctrl 键点击显示的网址以打开该网址:
    gcloud run services list \
         --format='value(URL)' \
         --filter='SERVICE:"codelab-o11y-service"'
    
    打开网址时,您可能会收到 500 错误或看到以下消息:
    Sorry, this is just a placeholder...
    
    这表示服务未完成部署。请稍等片刻,然后刷新页面。最后,您会看到以 Dog Fun Facts(狗狗趣事)开头的文字,其中包含 10 条与狗狗有关的趣事。

尝试与应用互动,了解各种动物的趣味知识。为此,请将 animal 参数附加到网址,例如 ?animal=[ANIMAL],其中 [ANIMAL] 是动物名称。例如,附加 ?animal=cat 可获取 10 条关于猫的趣事,附加 ?animal=sea turtle 可获取 10 条关于海龟的趣事。

7. 审核您的 Vertex API 调用

通过审核 Google API 调用,您可以解答“哪些用户何时在何处调用了特定 API”等问题。在排查应用问题、调查资源消耗或执行软件取证分析时,请务必进行审核。

借助审核日志,您可以跟踪管理员活动和系统活动,以及记录对“数据读取”和“数据写入”API 操作的调用。如需审核生成内容的 Vertex AI 请求,您必须在 Cloud 控制台中启用“数据读取”审核日志

  1. 点击下面的按钮,在 Cloud 控制台中打开“审核日志”页面

  2. 确保该页面已选择您为本实验创建的项目。所选项目显示在页面左上角三线状菜单的右侧:
    Google Cloud 控制台项目下拉菜单
    如有必要,请从下拉菜单中选择正确的项目。
  3. 数据访问审核日志配置表格中,找到“服务”列中的 Vertex AI API 服务,然后选中服务名称左侧的复选框以选择该服务。
    选择 Vertex AI API
  4. 在右侧的信息面板中,选择“数据读取”审核类型。
    查看数据读取日志
  5. 点击保存

如需生成审核日志,请打开服务网址。刷新页面,同时更改 ?animal= 参数的值,以获取不同的结果。

浏览审核日志

  1. 点击下面的按钮,在 Cloud 控制台中打开“日志浏览器”页面:

  2. 将以下过滤条件粘贴到“查询”窗格中。
    LOG_ID("cloudaudit.googleapis.com%2Fdata_access") AND
    protoPayload.serviceName="aiplatform.googleapis.com"
    
    “查询”窗格是位于“日志浏览器”页面顶部附近的编辑器:
    查询审核日志
  3. 点击运行查询
  4. 选择其中一个审核日志条目,然后展开相应字段以检查日志中捕获的信息。
    您可以查看 Vertex API 调用的详细信息,包括所用的方法和模型。您还可以查看调用方的身份以及授权调用的权限。

8. 记录与生成式 AI 的互动

您在审核日志中找不到 API 请求参数或响应数据。不过,这些信息对于排查应用和工作流分析问题可能很重要。在此步骤中,我们将通过添加应用日志记录来填补这一空白。

实现使用 Logback 与 Spring Boot 结合将应用日志输出到标准输出。此方法利用 Cloud Run 的功能,捕获输出到标准输出的信息,并自动将其提取到 Cloud Logging。为了以结构化数据的形式捕获信息,应相应地设置输出的日志格式。请按照以下说明为应用添加结构化日志记录功能。

  1. 返回浏览器中的“Cloud Shell”窗口(或标签页)。
  2. 在 Cloud Shell Editor 中创建并打开新文件 LoggingEventGoogleCloudEncoder.java
    cloudshell edit "${HOME}/codelab-o11y/src/main/java/com/example/demo/LoggingEventGoogleCloudEncoder.java"
    
  3. 复制并粘贴以下代码,以实现 Logback 编码器,该编码器会按照 Google Cloud 结构化日志格式将日志编码为字符串化 JSON:
    package com.example.demo;
    
    import static ch.qos.logback.core.CoreConstants.UTF_8_CHARSET;
    
    import java.time.Instant;
    import ch.qos.logback.core.encoder.EncoderBase;
    import ch.qos.logback.classic.Level;
    import ch.qos.logback.classic.spi.ILoggingEvent;
    import java.util.HashMap;
    
    import com.google.gson.Gson;
    
    public class LoggingEventGoogleCloudEncoder extends EncoderBase<ILoggingEvent>  {
        private static final byte[] EMPTY_BYTES = new byte[0];
        private final Gson gson = new Gson();
    
        @Override
        public byte[] headerBytes() {
            return EMPTY_BYTES;
        }
    
        @Override
        public byte[] encode(ILoggingEvent e) {
            var timestamp = Instant.ofEpochMilli(e.getTimeStamp());
            var fields = new HashMap<String, Object>() {
                {
                    put("timestamp", timestamp.toString());
                    put("severity", severityFor(e.getLevel()));
                    put("message", e.getMessage());
                }
            };
            var params = e.getKeyValuePairs();
            if (params != null && params.size() > 0) {
                params.forEach(kv -> fields.putIfAbsent(kv.key, kv.value));
            }
            var data = gson.toJson(fields) + "\n";
            return data.getBytes(UTF_8_CHARSET);
        }
    
        @Override
        public byte[] footerBytes() {
            return EMPTY_BYTES;
        }
    
        private static String severityFor(Level level) {
            switch (level.toInt()) {
                case Level.TRACE_INT:
                return "DEBUG";
                case Level.DEBUG_INT:
                return "DEBUG";
                case Level.INFO_INT:
                return "INFO";
                case Level.WARN_INT:
                return "WARNING";
                case Level.ERROR_INT:
                return "ERROR";
                default:
                return "DEFAULT";
            }
        }
    }
    
  4. 在 Cloud Shell Editor 中创建并打开新文件 logback.xml
    cloudshell edit "${HOME}/codelab-o11y/src/main/resources/logback.xml"
    
  5. 复制并粘贴以下 XML,将 Logback 配置为将编码器与将日志输出到标准输出的 Logback 附加器搭配使用:
    <?xml version="1.0" encoding="UTF-8"?>
    <configuration debug="true">
        <appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
            <encoder class="com.example.demo.LoggingEventGoogleCloudEncoder"/>
        </appender>
    
        <root level="info">
            <appender-ref ref="Console" />
        </root>
    </configuration>
    
  6. 在 Cloud Shell Editor 中重新打开 DemoApplication.java 文件:
    cloudshell edit "${HOME}/codelab-o11y/src/main/java/com/example/demo/DemoApplication.java"
    
  7. 将编辑器中的代码替换为下方所示的版本,以记录 Gen AI 请求和响应。如需替换代码,请删除文件的内容,然后将以下代码复制到编辑器中:
    package com.example.demo;
    
    import java.io.IOException;
    import java.util.Collections;
    
    import javax.annotation.PostConstruct;
    import javax.annotation.PreDestroy;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.google.cloud.ServiceOptions;
    import com.google.cloud.vertexai.VertexAI;
    import com.google.cloud.vertexai.api.GenerateContentResponse;
    import com.google.cloud.vertexai.generativeai.GenerativeModel;
    import com.google.cloud.vertexai.generativeai.ResponseHandler;
    
    @SpringBootApplication
    public class DemoApplication {
    
        public static void main(String[] args) {
            String port = System.getenv().getOrDefault("PORT", "8080");
            SpringApplication app = new SpringApplication(DemoApplication.class);
            app.setDefaultProperties(Collections.singletonMap("server.port", port));
            app.run(args);
        }
    }
    
    @RestController
    class HelloController {
        private final String projectId = ServiceOptions.getDefaultProjectId();
        private VertexAI vertexAI;
        private GenerativeModel model;
        private final Logger LOGGER = LoggerFactory.getLogger(HelloController.class);
    
        @PostConstruct
        public void init() {
            vertexAI = new VertexAI(projectId, "us-central1");
            model = new GenerativeModel("gemini-1.5-flash", vertexAI);
        }
    
        @PreDestroy
        public void destroy() {
            vertexAI.close();
        }
    
        @GetMapping("/")
        public String getFacts(@RequestParam(defaultValue = "dog") String animal) throws IOException {
            String prompt = "Give me 10 fun facts about " + animal + ". Return this as html without backticks.";
            GenerateContentResponse response = model.generateContent(prompt);
            LOGGER.atInfo()
                    .addKeyValue("animal", animal)
                    .addKeyValue("prompt", prompt)
                    .addKeyValue("response", response)
                    .log("Content is generated");
            return ResponseHandler.getText(response);
        }
    }
    

几秒钟后,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 服务网址复制到浏览器中的单独标签页或窗口中。或者,您也可以在终端中运行以下命令以输出服务网址,然后按住 Ctrl 键点击显示的网址以打开该网址:
    gcloud run services list \
         --format='value(URL)' \
         --filter='SERVICE:"codelab-o11y-service"'
    
    打开网址时,您可能会收到 500 错误或看到以下消息:
    Sorry, this is just a placeholder...
    
    这表示服务未完成部署。请稍等片刻,然后刷新页面。最后,您会看到以 Dog Fun Facts(狗狗趣事)开头的文字,其中包含 10 条与狗狗有关的趣事。

如需生成应用日志,请打开服务网址。刷新页面,同时更改 ?animal= 参数的值,以获取不同的结果。
如需查看应用日志,请执行以下操作:

  1. 点击下面的按钮,在 Cloud 控制台中打开“日志浏览器”页面:

  2. 将以下过滤条件粘贴到“查询”窗格(Logs Explorer 界面中的 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. 查看我们在上一步中使用过的 Logs Explorer 窗口。在“查询”窗格下,找到操作下拉菜单,然后点击该菜单以将其打开。请参阅以下屏幕截图,找到该菜单:
    包含“操作”下拉菜单的查询结果工具栏
  2. 在打开的菜单中,选择创建指标以打开创建基于日志的指标面板。
  3. 如需在创建基于日志的指标面板中配置新的计数器指标,请按以下步骤操作:
    1. 设置指标类型:选择计数器
    2. 详细信息部分中设置以下字段:
      • 日志指标名称:将名称设置为 model_interaction_count。您需遵循一些命名限制;如需了解详情,请参阅命名限制 问题排查
      • 说明:输入指标的说明。例如 Number of log entries capturing successful call to model inference.
      • 单位:请将此字段留空或插入数字 1
    3. 过滤器选择部分中,保留相应值。请注意,build 过滤条件字段与我们用于查看应用日志的过滤条件相同。
    4. (可选)添加一个标签,以帮助统计每只动物的呼叫次数。注意:此标签可能会大幅增加指标的基数,不建议在生产环境中使用:
      1. 点击添加标签
      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. 验证已在语言切换开关中选择 PromQL。语言切换开关位于同一工具栏中,用于设置查询的格式。
  4. 查询编辑器中输入查询:
    sum(rate(logging_googleapis_com:user_model_interaction_count{monitored_resource="cloud_run_revision"}[${__interval}]))
    
    如需详细了解如何使用 PromQL,请参阅 Cloud Monitoring 中的 PromQL
  5. 点击运行查询。您会看到一个类似于以下屏幕截图的折线图:
    显示查询的指标

    请注意,启用自动运行切换开关后,系统不会显示运行查询按钮。

10. (可选)使用 OpenTelemetry 进行监控和跟踪

如上一步所述,您可以使用 OpenTelemetry (Otel) SDK 实现指标。建议在多服务架构中使用 OTel。此步骤演示了如何将 OTel 插桩添加到 Spring Boot 应用。在此步骤中,您将执行以下操作:

  • 使用自动跟踪功能插桩 Spring Boot 应用
  • 实现计数器指标以监控成功的模型调用次数
  • 将跟踪记录与应用日志相关联

对于产品级服务,建议的架构是使用 OTel 收集器收集和提取来自多个服务的所有可观测性数据。为简单起见,此步骤中的代码不使用收集器。而是使用会将数据直接写入 Google Cloud 的 Otel 导出功能。

使用 OTel 组件和自动跟踪功能设置 Spring Boot 应用

  1. 返回浏览器中的“Cloud Shell”窗口(或标签页)。
  2. 在终端中,使用其他配置参数更新 application.permissions 文件:
    cat >> "${HOME}/codelab-o11y/src/main/resources/application.properties" << EOF
    otel.logs.exporter=none
    otel.traces.exporter=google_cloud_trace
    otel.metrics.exporter=google_cloud_monitoring
    otel.resource.attributes.service.name=codelab-o11y-service
    otel.traces.sampler=always_on
    EOF
    
    这些参数用于定义将可观测性数据导出到 Cloud Trace 和 Cloud Monitoring,并强制对所有轨迹进行采样。
  3. 将所需的 OpenTelemetry 依赖项添加到 pom.xml 文件中:
    sed -i 's/<dependencies>/<dependencies>\
    \
            <dependency>\
                <groupId>io.opentelemetry.instrumentation<\/groupId>\
                <artifactId>opentelemetry-spring-boot-starter<\/artifactId>\
            <\/dependency>\
            <dependency>\
                <groupId>com.google.cloud.opentelemetry<\/groupId>\
                <artifactId>exporter-auto<\/artifactId>\
                <version>0.33.0-alpha<\/version>\
            <\/dependency>\
            <dependency>\
                <groupId>com.google.cloud.opentelemetry<\/groupId>\
                <artifactId>exporter-trace<\/artifactId>\
                <version>0.33.0<\/version>\
            <\/dependency>\
            <dependency>\
                <groupId>com.google.cloud.opentelemetry<\/groupId>\
                <artifactId>exporter-metrics<\/artifactId>\
                <version>0.33.0<\/version>\
            <\/dependency>\
    /g' "${HOME}/codelab-o11y/pom.xml"
    
  4. 将 OpenTelemetry BOM 添加到 pom.xml 文件:
    sed -i 's/<\/properties>/<\/properties>\
        <dependencyManagement>\
            <dependencies>\
                <dependency>\
                    <groupId>io.opentelemetry.instrumentation<\/groupId>\
                    <artifactId>opentelemetry-instrumentation-bom<\/artifactId>\
                    <version>2.12.0<\/version>\
                    <type>pom<\/type>\
                    <scope>import<\/scope>\
                <\/dependency>\
            <\/dependencies>\
        <\/dependencyManagement>\
    /g' "${HOME}/codelab-o11y/pom.xml"
    
  5. 在 Cloud Shell Editor 中重新打开 DemoApplication.java 文件:
    cloudshell edit "${HOME}/codelab-o11y/src/main/java/com/example/demo/DemoApplication.java"
    
  6. 将当前代码替换为用于递增效果指标的版本。如需替换代码,请删除文件的内容,然后将以下代码复制到编辑器中:
    package com.example.demo;
    
    import io.opentelemetry.api.common.AttributeKey;
    import io.opentelemetry.api.common.Attributes;
    import io.opentelemetry.api.OpenTelemetry;
    import io.opentelemetry.api.metrics.LongCounter;
    
    import java.io.IOException;
    import java.util.Collections;
    
    import javax.annotation.PostConstruct;
    import javax.annotation.PreDestroy;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.google.cloud.ServiceOptions;
    import com.google.cloud.vertexai.VertexAI;
    import com.google.cloud.vertexai.api.GenerateContentResponse;
    import com.google.cloud.vertexai.generativeai.GenerativeModel;
    import com.google.cloud.vertexai.generativeai.ResponseHandler;
    
    
    @SpringBootApplication
    public class DemoApplication {
    
        public static void main(String[] args) {
            String port = System.getenv().getOrDefault("PORT", "8080");
            SpringApplication app = new SpringApplication(DemoApplication.class);
            app.setDefaultProperties(Collections.singletonMap("server.port", port));
            app.run(args);
        }
    }
    
    @RestController
    class HelloController {
        private final String projectId = ServiceOptions.getDefaultProjectId();
        private VertexAI vertexAI;
        private GenerativeModel model;
        private final Logger LOGGER = LoggerFactory.getLogger(HelloController.class);
        private static final String INSTRUMENTATION_NAME = "genai-o11y/java/workshop/example";
        private static final AttributeKey<String> ANIMAL = AttributeKey.stringKey("animal");
        private final LongCounter counter;
    
        public HelloController(OpenTelemetry openTelemetry) {
            this.counter = openTelemetry.getMeter(INSTRUMENTATION_NAME)
                    .counterBuilder("model_call_counter")
                    .setDescription("Number of successful model calls")
                    .build();
        }
    
        @PostConstruct
        public void init() {
            vertexAI = new VertexAI(projectId, "us-central1");
            model = new GenerativeModel("gemini-1.5-flash", vertexAI);
        }
    
        @PreDestroy
        public void destroy() {
            vertexAI.close();
        }
    
        @GetMapping("/")
        public String getFacts(@RequestParam(defaultValue = "dog") String animal) throws IOException {
            String prompt = "Give me 10 fun facts about " + animal + ". Return this as html without backticks.";
            GenerateContentResponse response = model.generateContent(prompt);
            LOGGER.atInfo()
                    .addKeyValue("animal", animal)
                    .addKeyValue("prompt", prompt)
                    .addKeyValue("response", response)
                    .log("Content is generated");
            counter.add(1, Attributes.of(ANIMAL, animal));
            return ResponseHandler.getText(response);
        }
    }
    
  7. 在 Cloud Shell Editor 中重新打开 LoggingEventGoogleCloudEncoder.java 文件:
    cloudshell edit "${HOME}/codelab-o11y/src/main/java/com/example/demo/LoggingEventGoogleCloudEncoder.java"
    
  8. 将当前代码替换为向写入的日志添加跟踪属性的版本。添加这些属性后,日志便可与正确的轨迹跨度相关联。如需替换代码,请删除文件的内容,然后将以下代码复制到编辑器中:
    package com.example.demo;
    
    import static ch.qos.logback.core.CoreConstants.UTF_8_CHARSET;
    
    import java.time.Instant;
    import java.util.HashMap;
    
    import ch.qos.logback.core.encoder.EncoderBase;
    import ch.qos.logback.classic.Level;
    import ch.qos.logback.classic.spi.ILoggingEvent;
    import com.google.cloud.ServiceOptions;
    import io.opentelemetry.api.trace.Span;
    import io.opentelemetry.api.trace.SpanContext;
    import io.opentelemetry.context.Context;
    
    import com.google.gson.Gson;
    
    
    public class LoggingEventGoogleCloudEncoder extends EncoderBase<ILoggingEvent>  {
        private static final byte[] EMPTY_BYTES = new byte[0];
        private final Gson gson;
        private final String projectId;
        private final String tracePrefix;
    
    
        public LoggingEventGoogleCloudEncoder() {
            this.gson = new Gson();
            this.projectId = lookUpProjectId();
            this.tracePrefix = "projects/" + (projectId == null ? "" : projectId) + "/traces/";
        }
    
        private static String lookUpProjectId() {
            return ServiceOptions.getDefaultProjectId();
        }
    
        @Override
        public byte[] headerBytes() {
            return EMPTY_BYTES;
        }
    
        @Override
        public byte[] encode(ILoggingEvent e) {
            var timestamp = Instant.ofEpochMilli(e.getTimeStamp());
            var fields = new HashMap<String, Object>() {
                {
                    put("timestamp", timestamp.toString());
                    put("severity", severityFor(e.getLevel()));
                    put("message", e.getMessage());
                    SpanContext context = Span.fromContext(Context.current()).getSpanContext();
                    if (context.isValid()) {
                        put("logging.googleapis.com/trace", tracePrefix + context.getTraceId());
                        put("logging.googleapis.com/spanId", context.getSpanId());
                        put("logging.googleapis.com/trace_sampled", Boolean.toString(context.isSampled()));
                    }
                }
            };
            var params = e.getKeyValuePairs();
            if (params != null && params.size() > 0) {
                params.forEach(kv -> fields.putIfAbsent(kv.key, kv.value));
            }
            var data = gson.toJson(fields) + "\n";
            return data.getBytes(UTF_8_CHARSET);
        }
    
        @Override
        public byte[] footerBytes() {
            return EMPTY_BYTES;
        }
    
        private static String severityFor(Level level) {
            switch (level.toInt()) {
                case Level.TRACE_INT:
                return "DEBUG";
                case Level.DEBUG_INT:
                return "DEBUG";
                case Level.INFO_INT:
                return "INFO";
                case Level.WARN_INT:
                return "WARNING";
                case Level.ERROR_INT:
                return "ERROR";
                default:
                return "DEFAULT";
            }
        }
    }
    

几秒钟后,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 服务网址复制到浏览器中的单独标签页或窗口中。或者,您也可以在终端中运行以下命令以输出服务网址,然后按住 Ctrl 键点击显示的网址以打开该网址:
    gcloud run services list \
         --format='value(URL)' \
         --filter='SERVICE:"codelab-o11y-service"'
    
    打开网址时,您可能会收到 500 错误或看到以下消息:
    Sorry, this is just a placeholder...
    
    这表示服务未完成部署。请稍等片刻,然后刷新页面。最后,您会看到以 Dog Fun Facts(狗狗趣事)开头的文字,其中包含 10 条与狗狗有关的趣事。

如需生成遥测数据,请打开服务网址。刷新页面,同时更改 ?animal= 参数的值,以获取不同的结果。

探索应用轨迹

  1. 点击下面的按钮,在 Cloud 控制台中打开 Trace 探索器页面:

  2. 选择最近的轨迹之一。您应该会看到 5 到 6 个 span,如下面的屏幕截图所示。
    Trace 探索器中的应用 span 视图
  3. 找到用于跟踪对事件处理脚本(fun_facts 方法)的调用的 span。它将是最后一个名称为 / 的 span。
  4. 跟踪记录详情窗格中,选择日志和事件。您会看到与此特定 span 相关联的应用日志。系统会使用跟踪记录和日志中的跟踪记录 ID 和 Span 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. 创建一个日志接收器,将提取的日志重定向到 Pub/Sub 主题。
  3. 按照以下步骤创建一个 Dataflow 流水线,用于修改重定向到 Pub/Sub 主题的日志:
    1. 从 Pub/Sub 主题读取日志条目
    2. 使用 DLP 检查 API 检查条目的载荷是否包含敏感信息
    3. 使用某种 DLP 隐去方法隐去载荷中的敏感信息
    4. 将经过混淆处理的日志条目写入 Cloud Logging
  4. 部署流水线。

12. (可选)清理

为避免因本 Codelab 中使用的资源和 API 而产生费用,建议您在完成实验后进行清理。若要避免产生费用,最简单的方法是删除您为此 Codelab 创建的项目。

  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) 调研,以便改进您目前使用的 Google 产品,请点击此处报名

以下是继续学习的几种方法: