1. 개요
생성형 AI 애플리케이션에는 다른 애플리케이션과 마찬가지로 관측 가능성도 필요합니다. 생성형 AI에 특별한 관측 기술이 필요한가요?
이 실습에서는 간단한 생성형 AI 애플리케이션을 만듭니다. Cloud Run에 배포합니다. Google Cloud 관측 가능성 서비스 및 제품을 사용하여 필수 모니터링 및 로깅 기능으로 계측합니다.
학습할 내용
- Cloud Shell 편집기를 사용하여 Vertex AI를 사용하는 애플리케이션 작성
- GitHub에 애플리케이션 코드 저장
- gcloud CLI를 사용하여 애플리케이션의 소스 코드를 Cloud Run에 배포
- 생성형 AI 애플리케이션에 모니터링 및 로깅 기능 추가
- 로그 기반 측정항목 사용
- OpenTelemetry SDK로 로깅 및 모니터링 구현
- 책임감 있는 AI 데이터 처리에 대한 유용한 정보 얻기
2. 기본 요건
아직 Google 계정이 없다면 새 계정을 만들어야 합니다.
3. 프로젝트 설정
- Google 계정으로 Google Cloud 콘솔에 로그인합니다.
- 새 프로젝트를 만들거나 기존 프로젝트를 재사용합니다. 방금 만들었거나 선택한 프로젝트의 프로젝트 ID를 기록합니다.
- 프로젝트에 결제를 사용 설정합니다.
- 이 실습을 완료하는 데는 청구 비용이 $5 미만이 소요됩니다.
- 이 실습의 끝에 있는 단계에 따라 리소스를 삭제하여 추가 비용이 청구되지 않도록 할 수 있습니다.
- 신규 사용자는 미화 300달러 상당의 무료 체험판을 이용할 수 있습니다.
- Cloud Billing의 내 프로젝트에서 결제가 사용 설정되어 있는지 확인합니다.
- 새 프로젝트의
Billing account
열에Billing is disabled
가 표시되면 다음 단계를 따르세요.Actions
열에서 점 3개를 클릭합니다.- 결제 변경을 클릭합니다.
- 사용할 결제 계정을 선택합니다.
- 라이브 이벤트에 참석하는 경우 계정 이름이 Google Cloud Platform 평가판 결제 계정일 수 있습니다.
- 새 프로젝트의
4. Cloud Shell 편집기 준비
- Cloud Shell 편집기로 이동합니다. Cloud Shell에서 사용자 인증 정보로 gcloud를 호출하도록 승인하라는 메시지가 표시되면 승인을 클릭하여 계속합니다.
- 터미널 창 열기
- 햄버거 메뉴
를 클릭합니다.
- 터미널을 클릭합니다.
- 새 터미널
을 클릭합니다.
- 햄버거 메뉴
- 터미널에서 프로젝트 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. 생성형 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 편집기에서 파일을 엽니다. 초기화 코드를 호스팅하는 데 사용됩니다. 이름이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 편집기에서
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 서비스 URL을 브라우저의 별도 탭 또는 창에 복사합니다. 또는 터미널에서 다음 명령어를 실행하여 서비스 URL을 출력하고 Ctrl 키를 누른 상태에서 표시된 URL을 클릭하여 URL을 엽니다.
URL을 열면 500 오류가 발생하거나 다음 메시지가 표시될 수 있습니다.gcloud run services list \ --format='value(URL)' \ --filter='SERVICE:"codelab-o11y-service"'
서비스 배포가 완료되지 않았음을 의미합니다. 잠시 기다린 후 페이지를 새로고침합니다. 끝에는 재미있는 강아지 정보로 시작하는 텍스트와 강아지에 관한 재미있는 정보 10가지가 표시됩니다.Sorry, this is just a placeholder...
애플리케이션과 상호작용하여 다양한 동물에 관한 재미있는 사실을 알아보세요. 이렇게 하려면 animal
매개변수를 URL에 추가합니다(예: ?animal=[ANIMAL]
, 여기서 [ANIMAL]
는 동물 이름임). 예를 들어 ?animal=cat
를 추가하면 고양이에 관한 재미있는 사실 10가지를, ?animal=sea turtle
를 추가하면 바다거북에 관한 재미있는 사실 10가지를 확인할 수 있습니다.
7. Vertex API 호출 감사
Google API 호출을 감사하면 '누가, 언제, 어디서, 특정 API를 호출했는지'와 같은 질문에 답할 수 있습니다. 감사는 애플리케이션 문제를 해결하거나, 리소스 사용량을 조사하거나, 소프트웨어 포렌식 분석을 수행할 때 중요합니다.
감사 로그를 사용하면 관리 및 시스템 활동을 추적하고 '데이터 읽기' 및 '데이터 쓰기' API 작업 호출을 로깅할 수 있습니다. 콘텐츠 생성을 위한 Vertex AI 요청을 감사하려면 Cloud 콘솔에서 '데이터 읽기' 감사 로그를 사용 설정해야 합니다.
- 아래 버튼을 클릭하여 Cloud 콘솔에서 감사 로그 페이지를 엽니다.
- 페이지에 이 실습용으로 만든 프로젝트가 선택되어 있는지 확인합니다. 선택한 프로젝트는 햄버거 메뉴 바로 옆에 있는 페이지 왼쪽 상단에 표시됩니다.
필요한 경우 콤보박스에서 올바른 프로젝트를 선택합니다. - 데이터 액세스 감사 로그 구성 표의 서비스 열에서
Vertex AI API
서비스를 찾고 서비스 이름 왼쪽에 있는 체크박스를 선택하여 서비스를 선택합니다. - 오른쪽의 정보 패널에서 '데이터 읽기' 감사 유형을 선택합니다.
- 저장을 클릭합니다.
감사 로그를 생성하려면 서비스 URL을 엽니다. ?animal=
매개변수의 값을 변경하면서 페이지를 새로고침하여 다른 결과를 가져옵니다.
감사 로그 살펴보기
- 아래 버튼을 클릭하여 Cloud 콘솔에서 로그 탐색기 페이지를 엽니다.
- 다음 필터를 쿼리 창에 붙여넣습니다.
쿼리 창은 로그 탐색기 페이지 상단에 있는 편집기입니다.LOG_ID("cloudaudit.googleapis.com%2Fdata_access") AND protoPayload.serviceName="aiplatform.googleapis.com"
- 쿼리 실행을 클릭합니다.
- 감사 로그 항목 중 하나를 선택하고 필드를 펼쳐 로그에 캡처된 정보를 검사합니다.
메서드 및 사용된 모델을 비롯한 Vertex API 호출에 관한 세부정보를 확인할 수 있습니다. 호출자의 ID와 호출을 승인한 권한도 확인할 수 있습니다.
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 Logging 에이전트가 로그를 수집하고 Cloud Logging에 비동기식으로 처리합니다. main()
함수가 수정되어 구조화된 형식 지정 가이드라인을 따르는 JSON 스키마를 사용하도록 Go 표준 구조화 로그를 설정합니다. 모든 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 서비스 URL을 브라우저의 별도 탭 또는 창에 복사합니다. 또는 터미널에서 다음 명령어를 실행하여 서비스 URL을 출력하고 Ctrl 키를 누른 상태에서 표시된 URL을 클릭하여 URL을 엽니다.
URL을 열면 500 오류가 발생하거나 다음 메시지가 표시될 수 있습니다.gcloud run services list \ --format='value(URL)' \ --filter='SERVICE:"codelab-o11y-service"'
서비스 배포가 완료되지 않았음을 의미합니다. 잠시 기다린 후 페이지를 새로고침합니다. 끝에는 재미있는 강아지 정보로 시작하는 텍스트와 강아지에 관한 재미있는 정보 10가지가 표시됩니다.Sorry, this is just a placeholder...
애플리케이션 로그를 생성하려면 서비스 URL을 엽니다. ?animal=
매개변수의 값을 변경하면서 페이지를 새로고침하여 다른 결과를 가져옵니다.
애플리케이션 로그를 보려면 다음 단계를 따르세요.
- 아래 버튼을 클릭하여 Cloud 콘솔에서 로그 탐색기 페이지를 엽니다.
- 다음 필터를 쿼리 창(로그 탐색기 인터페이스의 2번)에 붙여넣습니다.
LOG_ID("run.googleapis.com%2Fstdout") AND severity=DEBUG
- 쿼리 실행을 클릭합니다.
쿼리 결과에는 프롬프트와 안전 등급을 포함한 Vertex AI 응답이 포함된 로그가 표시됩니다.
9. 생성형 AI와의 상호작용 수 집계
Cloud Run은 배포된 서비스를 모니터링하는 데 사용할 수 있는 관리형 측정항목을 기록합니다. 사용자가 관리하는 모니터링 측정항목을 사용하면 데이터와 측정항목 업데이트 빈도를 더 효과적으로 관리할 수 있습니다. 이러한 측정항목을 구현하려면 데이터를 수집하여 Cloud Monitoring에 작성하는 코드를 작성해야 합니다. OpenTelemetry SDK를 사용하여 구현하는 방법은 다음 (선택사항) 단계를 참고하세요.
이 단계에서는 코드에서 사용자 측정항목을 구현하는 대안인 로그 기반 측정항목을 보여줍니다. 로그 기반 측정항목을 사용하면 애플리케이션이 Cloud Logging에 작성하는 로그 항목에서 모니터링 측정항목을 생성할 수 있습니다. 이전 단계에서 구현한 애플리케이션 로그를 사용하여 유형 카운터의 로그 기반 측정항목을 정의합니다. 이 측정항목은 Vertex API를 성공적으로 호출한 횟수를 집계합니다.
- 이전 단계에서 사용한 로그 탐색기 창을 살펴봅니다. 쿼리 창에서 작업 드롭다운 메뉴를 찾아 클릭하여 엽니다. 아래 스크린샷을 참고하여 메뉴를 찾습니다.
- 열린 메뉴에서 측정항목 만들기를 선택하여 로그 기반 측정항목 만들기 패널을 엽니다.
- 로그 기반 측정항목 만들기 패널에서 새 카운터 측정항목을 구성하려면 다음 단계를 따르세요.
- 측정항목 유형 설정: 카운터를 선택합니다.
- 세부정보 섹션에서 다음 필드를 설정합니다.
- 로그 측정항목 이름: 이름을
model_interaction_count
로 설정합니다. 이름 지정 시 특정 제한사항이 적용됩니다. 자세한 내용은 이름 지정 제한사항 문제 해결을 참고하세요. - 설명: 측정항목에 대한 설명을 입력합니다. 예를 들면
Number of log entries capturing successful call to model inference.
입니다. - 단위: 이 필드를 공백으로 남겨두거나 숫자
1
을 삽입하세요.
- 로그 측정항목 이름: 이름을
- 필터 선택 섹션의 값은 그대로 둡니다. 빌드 필터 필드에는 애플리케이션 로그를 확인하는 데 사용한 것과 동일한 필터가 있습니다.
- (선택사항) 각 동물의 호출 수를 집계하는 데 도움이 되는 라벨을 추가합니다. 참고: 이 라벨은 측정항목의 카디널리티를 크게 늘릴 수 있으며 프로덕션에서는 사용하지 않는 것이 좋습니다.
- 라벨 추가를 클릭합니다.
- 라벨 섹션에서 다음 필드를 설정합니다.
- 라벨 이름: 이름을
animal
로 설정합니다. - 설명: 라벨에 대한 설명을 입력합니다. 예를 들면
Animal parameter
입니다. - 라벨 유형:
STRING
를 선택합니다. - 필드 이름:
jsonPayload.animal
을 입력합니다. - 정규 표현식: 비워둡니다.
- 라벨 이름: 이름을
- 완료를 클릭합니다.
- 측정항목 만들기를 클릭하여 측정항목을 만듭니다.
gcloud logging metrics create
CLI 명령어를 사용하거나 google_logging_metric
Terraform 리소스를 사용하여 로그 기반 측정항목 페이지에서 로그 기반 측정항목을 만들 수도 있습니다.
측정항목 데이터를 생성하려면 서비스 URL을 엽니다. 열린 페이지를 여러 번 새로고침하여 모델을 여러 번 호출합니다. 이전과 마찬가지로 매개변수에 다른 동물을 사용해 봅니다.
PromQL 쿼리를 입력하여 로그 기반 측정항목 데이터를 검색합니다. PromQL 쿼리를 입력하려면 다음 단계를 따르세요.
- 아래 버튼을 클릭하여 Cloud 콘솔에서 측정항목 탐색기 페이지를 엽니다.
- 쿼리 빌더 창의 툴바에서 이름이 < > MQL 또는 < > PromQL인 버튼을 선택합니다. 버튼 위치는 아래 그림을 참고하세요.
- 언어 전환 버튼에 PromQL이 선택되어 있는지 확인합니다. 언어 전환 버튼은 쿼리 형식을 지정할 수 있는 동일한 툴바에 있습니다.
- 쿼리 편집기에 쿼리를 입력합니다.
PromQL 사용에 관한 자세한 내용은 Cloud Monitoring의 PromQL을 참고하세요.sum(rate(logging_googleapis_com:user_model_interaction_count{monitored_resource="cloud_run_revision"}[${__interval}]))
- 쿼리 실행을 클릭합니다. 다음 스크린샷과 유사한 선 차트가 표시됩니다.
자동 실행 전환 버튼이 사용 설정된 경우 쿼리 실행 버튼이 표시되지 않습니다.
10. (선택사항) 모니터링 및 추적에 Open Telemetry 사용
이전 단계에서 언급했듯이 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()
메서드는 트레이스 및 측정항목이 Google Cloud Trace 및 Monitoring에 직접 쓸 수 있도록 OpenTelemetry 내보내기 도구를 설정하도록 수정되었습니다. 또한 수집된 트레이스와 측정항목을 Cloud Run 환경과 관련된 메타데이터로 채우기 위한 추가 구성을 실행합니다. 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 서비스 URL을 브라우저의 별도 탭 또는 창에 복사합니다. 또는 터미널에서 다음 명령어를 실행하여 서비스 URL을 출력하고 Ctrl 키를 누른 상태에서 표시된 URL을 클릭하여 URL을 엽니다.
URL을 열면 500 오류가 발생하거나 다음 메시지가 표시될 수 있습니다.gcloud run services list \ --format='value(URL)' \ --filter='SERVICE:"codelab-o11y-service"'
서비스 배포가 완료되지 않았음을 의미합니다. 잠시 기다린 후 페이지를 새로고침합니다. 끝에는 재미있는 강아지 정보로 시작하는 텍스트와 강아지에 관한 재미있는 정보 10가지가 표시됩니다.Sorry, this is just a placeholder...
원격 분석 데이터를 생성하려면 서비스 URL을 엽니다. ?animal=
매개변수의 값을 변경하면서 페이지를 새로고침하여 다른 결과를 가져옵니다.
애플리케이션 트레이스 살펴보기
- 아래 버튼을 클릭하여 Cloud 콘솔에서 Trace 탐색기 페이지를 엽니다.
- 가장 최근 트레이스 중 하나를 선택합니다. 아래 스크린샷과 같이 5~6개의 스팬이 표시됩니다.
- 이벤트 핸들러 (
fun_facts
메서드) 호출을 추적하는 스팬을 찾습니다. 이름이/
인 마지막 스팬입니다. - 트레이스 세부정보 창에서 로그 및 이벤트를 선택합니다. 이 특정 스팬과 상관관계가 있는 애플리케이션 로그가 표시됩니다. 상관관계는 트레이스 및 로그의 트레이스 및 스팬 ID를 사용하여 감지됩니다. 프롬프트를 작성한 애플리케이션 로그와 Vertex API의 응답이 표시됩니다.
카운터 측정항목 살펴보기
- 아래 버튼을 클릭하여 Cloud 콘솔에서 측정항목 탐색기 페이지를 엽니다.
- 쿼리 빌더 창의 툴바에서 이름이 < > MQL 또는 < > PromQL인 버튼을 선택합니다. 버튼 위치는 아래 그림을 참고하세요.
- 언어 전환 버튼에 PromQL이 선택되어 있는지 확인합니다. 언어 전환 버튼은 쿼리 형식을 지정할 수 있는 동일한 툴바에 있습니다.
- 쿼리 편집기에 쿼리를 입력합니다.
sum(rate(workload_googleapis_com:model_call_counter{monitored_resource="generic_task"}[${__interval}]))
- 쿼리 실행을 클릭합니다.자동 실행 전환 버튼이 사용 설정되어 있으면 쿼리 실행 버튼이 표시되지 않습니다.
11. (선택사항) 로그에서 난독화된 민감한 정보
10단계에서 애플리케이션이 Gemini 모델과 상호작용하는 정보가 로깅되었습니다. 이 정보에는 동물의 이름, 실제 프롬프트, 모델의 응답이 포함되었습니다. 이 정보를 로그에 저장하는 것은 안전하지만 다른 많은 시나리오에서는 필요하지 않습니다. 프롬프트에는 사용자가 저장하고 싶지 않은 개인 정보 또는 민감한 정보가 포함될 수 있습니다. 이 문제를 해결하려면 Cloud Logging에 기록되는 민감한 정보를 난독화하면 됩니다. 코드 수정을 최소화하려면 다음 솔루션을 사용하는 것이 좋습니다.
- 수신 로그 항목을 저장할 PubSub 주제 만들기
- 처리된 로그를 PubSub 주제로 리디렉션하는 로그 싱크를 만듭니다.
- 다음 단계에 따라 PubSub 주제로 리디렉션된 로그를 수정하는 Dataflow 파이프라인을 만듭니다.
- Pub/Sub 주제에서 로그 항목 읽기
- DLP 검사 API를 사용하여 항목의 페이로드에서 민감한 정보를 검사합니다.
- DLP 수정 방법 중 하나를 사용하여 페이로드의 민감한 정보를 수정합니다.
- 난독화된 로그 항목을 Cloud Logging에 작성
- 파이프라인 배포
12. (선택사항) 정리
Codelab에서 사용한 리소스와 API에 대한 비용이 청구되지 않도록 하려면 실습을 완료한 후 정리하는 것이 좋습니다. 비용이 청구되지 않도록 하는 가장 쉬운 방법은 Codelab에서 만든 프로젝트를 삭제하는 것입니다.
- 프로젝트를 삭제하려면 터미널에서 delete project 명령어를 실행합니다.
클라우드 프로젝트를 삭제하면 해당 프로젝트 내에서 사용되는 모든 리소스 및 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 모델을 사용하여 예측하는 생성형 AI 애플리케이션을 만들었습니다. 필수 모니터링 및 로깅 기능으로 애플리케이션을 계측했습니다. 애플리케이션과 소스 코드의 변경사항을 Cloud Run에 배포했습니다. 그런 다음 Google Cloud Observability 제품을 사용하여 애플리케이션의 성능을 추적하여 애플리케이션의 안정성을 보장할 수 있습니다.
현재 사용 중인 제품을 개선하기 위한 사용자 경험 (UX) 연구에 참여하고 싶다면 여기에서 등록하세요.
다음은 학습을 계속할 수 있는 몇 가지 방법입니다.
- Codelab Cloud Run에 Gemini 지원 채팅 앱을 배포하는 방법
- Codelab Cloud Run에서 Gemini 함수 호출을 사용하는 방법
- Cloud Run Jobs Video Intelligence API를 사용하여 동영상을 장면별로 처리하는 방법
- 주문형 워크샵 Google Kubernetes Engine 온보딩
- 애플리케이션 로그를 사용하여 카운터 및 분포 측정항목을 구성하는 방법을 자세히 알아보세요.
- OpenTelemetry 사이드카를 사용하여 OTLP 측정항목 작성
- Google Cloud에서 OpenTelemetry를 사용하는 방법에 관한 참조