소개
AI 요청이 실패하면 호출자는 최종 HTTP 응답만 확인합니다. 이 응답으로 문제가 발생했다는 사실은 알 수 있지만, 요청 형식이 잘못된 것인지, 게이트웨이가 거부한 것인지, 업스트림 모델 공급자가 거부한 것인지 항상 알 수 있는 것은 아닙니다. **관측 가능성 (Observability)**은 요청이 호출자를 떠난 뒤에도 요청을 추적하고, 어떤 경계에서 처리되었는지 설명할 수 있을 만큼 충분한 증거를 수집하는 것을 의미합니다.
AI Gateway 는 게이트웨이에 도달한 요청마다 로그 항목 하나를 기록합니다. 로그에는 공급자, 모델, HTTP 상태, 소요 시간 및 토큰 사용량이 표시될 수 있습니다. 나중에 요청을 찾는 데 도움이 되는 몇 가지 **사용자 지정 메타데이터 (custom metadata)**도 추가할 수 있습니다. 메타데이터는 개인 정보를 보관하는 금고가 아닙니다. 이 실습에서는 임의의 trace ID, 합성 케이스 이름, Boolean 플래그만 사용하며 자격 증명, 프롬프트, 이메일 주소 또는 계정 ID 는 절대 저장하지 않습니다.
먼저 폐기할 인증 게이트웨이 하나를 만든 다음, 안전한 trace 레이블과 함께 의도적으로 형식이 잘못된 Workers AI 요청을 보냅니다. 실패한 로그를 찾고, 게이트웨이 인증 실패와 업스트림 인증 실패를 비교합니다. 그런 다음 입력을 수정하고 같은 trace 에서 이제 성공한 요청이 기록되는지 확인합니다. 이렇게 하면 추측이 아니라 증거를 바탕으로 문제를 해결할 수 있습니다.
이 과정에 직접 들어왔다면 먼저 Connect LabEx to Your Cloudflare Account를 완료합니다. 이 과정에서는 LabEx VM 터미널, Wrangler 디바이스 인증, 학습 계정 확인 및 명시적 계정 ID 를 다룹니다. 또한 이 실습은 두 개의 별도 인증 헤더를 사용하므로 먼저 Route Inference Through a Gateway를 완료합니다.
이 실습에서는 Cloudflare 에서 호스팅하는 @cf/meta/llama-3.3-70b-instruct-fp8-fast 모델과 Standard Workers AI 결제를 사용합니다. Workers Paid, Unified Billing 및 외부 공급자 계정은 필요하지 않습니다. 요청은 작고 합성된 데이터로 구성됩니다. 공유 일일 Workers AI 할당량을 사용할 수 없으면 반복해서 재시도하지 말고 중지합니다.
설정 과정에서 Node.js 22.22.0 과 프로젝트 전용 Wrangler 4.132.0 을 /home/labex/project/ai-gateway-trace에 설치합니다. 읽기 전용 평가를 독립적으로 준비하지만 Wrangler 인증, 클라우드 리소스 생성 또는 모델 트래픽 전송은 수행하지 않습니다. 실습이 끝나면 LabEx 가 임시 VM 을 삭제합니다. 하지만 VM 삭제만으로는 클라우드 리소스를 제거할 수 없으므로 로그아웃하기 전에 게이트웨이와 토큰을 직접 삭제합니다.
VM 인증 및 안전한 Trace ID 생성
이 단계에서는 새 VM 을 학습 계정에 연결하고, 폐기할 게이트웨이 하나와 합성 trace 하나에 사용할 이름을 만듭니다.
Trace ID는 서로 관련된 관측 결과를 연결하는 레이블입니다. 사용자가 입력한 내용이나 사용자의 신원을 노출하지 않으면서 요청을 식별할 수 있어야 합니다. 이 실습에서는 임의의 값을 생성하고, 자격 증명이 아닌 리소스 이름과 함께 저장합니다.
준비된 프로젝트로 이동해 고정된 CLI 버전을 확인하고 이 VM 을 인증합니다.
cd /home/labex/project/ai-gateway-trace
npx wrangler --version
npx wrangler login --device --browser=false --scopes account:read user:read ai:write
표시된 링크를 열고 코드를 입력한 다음 사용할 학습 계정을 인증합니다. 구조화된 ID 정보를 확인합니다.
npx wrangler whoami --json
Wrangler 4.132.0 과 loggedIn: true가 표시되어야 합니다. 아래의 YOUR_ACCOUNT_ID를 사용할 계정에 표시된 실제 32 자 ID 로 바꿉니다.
GATEWAY_ID="labex-c09-g02-$(openssl rand -hex 6)"
TOKEN_NAME="$GATEWAY_ID-token"
TRACE_ID="trace-$(openssl rand -hex 8)"
cat > .labex/state.json <<JSON
{
"accountId": "YOUR_ACCOUNT_ID",
"gatewayId": "$GATEWAY_ID",
"tokenName": "$TOKEN_NAME",
"traceId": "$TRACE_ID"
}
JSON
cat .labex/state.json
Trace ID 는 안전한 합성 데이터입니다. 계정 ID 와 리소스 이름은 로컬 상태 파일에 저장되므로 이후 정리 작업이 이 실습에서 만든 리소스만 대상으로 삼을 수 있습니다.
관측 가능한 인증 게이트웨이 생성
이 단계에서는 호출자 인증 경계를 통과한 요청을 기록하는 게이트웨이를 만듭니다.
Cloudflare Dashboard 를 열고 AI → AI Gateway → Create gateway → Custom gateway를 선택합니다. 저장한 gatewayId를 게이트웨이 이름으로 사용합니다. 요청 로깅과 게이트웨이 인증은 켜 둡니다. 캐시, 속도 제한, 지출 제한 및 재시도는 끄고, Workers AI 결제는 Standard로 유지합니다.
생성한 후 breadcrumb 에서 고유한 게이트웨이 ID 를 확인하고 Settings를 엽니다. 로깅은 이 실습에서 사용하는 증거를 생성하며, 인증은 알 수 없는 호출자가 로그를 쌓거나 모델 사용량을 소비하지 못하게 합니다.
Create an AI Gateway authentication token을 선택합니다. 저장한 tokenName을 사용하고, 사용할 학습 계정만 포함한 다음 다음 권한만 정확히 설정합니다.
- 인증된 게이트웨이에 진입하기 위한 AI Gateway — Run
- 로그를 읽고 폐기할 게이트웨이를 삭제하기 위한 AI Gateway — Edit
Workers AI 권한은 추가하지 않습니다. Wrangler 가 별도의 단기 업스트림 자격 증명을 제공합니다. 계정과 권한을 검토한 후 토큰을 생성하고, 토큰의 일회성 값을 화면에 출력하지 않은 채 저장합니다.
bash -c '
while :; do
read -rsp "Paste the AI Gateway token: " GATEWAY_TOKEN
printf "\n"
[ -n "$GATEWAY_TOKEN" ] && break
printf "Token cannot be empty; paste it again.\n" >&2
done
umask 077
printf "%s" "$GATEWAY_TOKEN" > .labex/gateway-token
unset GATEWAY_TOKEN
chmod 600 .labex/gateway-token
'
인증된 관리 API 를 통해 정확한 리소스를 확인합니다.
ACCOUNT_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).accountId')
GATEWAY_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).gatewayId')
GATEWAY_TOKEN=$(cat .labex/gateway-token)
curl --http1.1 -fsS \
-H "Authorization: Bearer $GATEWAY_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai-gateway/gateways/$GATEWAY_ID" \
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const b=JSON.parse(s),g=b.result||{};console.log(JSON.stringify({success:b.success,id:g.id,collect_logs:g.collect_logs,authentication:g.authentication},null,2))})'
unset GATEWAY_TOKEN
저장한 ID 와 함께 collect_logs: true, authentication: true가 표시되어야 합니다.
잘못된 입력으로 태그가 지정된 요청 보내기
이 단계에서는 통제된 입력 실패를 만듭니다. 게이트웨이와 업스트림 자격 증명은 유효한 상태로 유지하고, 모델 입력만 잘못된 형식으로 만듭니다.
사용자 지정 메타데이터에는 최대 5 개의 평면 문자열, 숫자 또는 Boolean 값만 사용할 수 있습니다. cf.로 시작하는 키는 Cloudflare 가 예약합니다. 이 요청에서는 안전한 값 세 가지를 사용합니다. 임의의 trace ID, bad-input 케이스 이름, synthetic: true입니다.
선택한 모델에는 프롬프트가 필요합니다. 프롬프트를 의도적으로 생략하고 응답과 HTTP 상태를 모두 저장합니다.
ACCOUNT_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).accountId')
GATEWAY_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).gatewayId')
TRACE_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).traceId')
MODEL='@cf/meta/llama-3.3-70b-instruct-fp8-fast'
METADATA=$(node -e 'process.stdout.write(JSON.stringify({trace_id:process.argv[1],case:"bad-input",synthetic:true}))' "$TRACE_ID")
GATEWAY_TOKEN=$(cat .labex/gateway-token)
UPSTREAM_TOKEN=$(npx wrangler auth token --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s).token))')
STATUS=$(curl --http1.1 -sS -D .labex/bad-input-headers.txt \
-o .labex/bad-input-response.json -w '%{http_code}' \
-H "cf-aig-authorization: Bearer $GATEWAY_TOKEN" \
-H "Authorization: Bearer $UPSTREAM_TOKEN" \
-H "cf-aig-metadata: $METADATA" \
-H 'Content-Type: application/json' \
--data '{"max_tokens":16}' \
"https://gateway.ai.cloudflare.com/v1/$ACCOUNT_ID/$GATEWAY_ID/workers-ai/$MODEL")
unset GATEWAY_TOKEN UPSTREAM_TOKEN METADATA
printf '%s\n' "$STATUS" | tee .labex/bad-input-status.txt
HTTP 400 또는 422가 표시되어야 합니다. 이는 클라이언트 입력 실패이며, 인증 문제의 증거가 아닙니다. 응답 본문은 문제를 제한적으로 조사할 수 있도록 저장되지만 자동으로 출력되지는 않습니다.
게이트웨이 로그와 실패 요청 연결
이 단계에서는 시간만 기준으로 검색하지 않고 trace ID 를 사용해 요청 기록을 찾습니다.
로그가 표시되기까지 잠시 걸릴 수 있습니다. 관리 API 에서 기존 로그 목록을 읽고, 각 평면 메타데이터 객체를 파싱한 다음 실패를 설명하는 데 필요한 필드만 출력합니다.
ACCOUNT_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).accountId')
GATEWAY_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).gatewayId')
TRACE_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).traceId')
GATEWAY_TOKEN=$(cat .labex/gateway-token)
curl --http1.1 -fsS \
-H "Authorization: Bearer $GATEWAY_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai-gateway/gateways/$GATEWAY_ID/logs?per_page=50" \
> .labex/logs-after-input.json
unset GATEWAY_TOKEN
node - <<'NODE'
const body = require('./.labex/logs-after-input.json')
const trace = require('./.labex/state.json').traceId
const meta = row => {
try { return typeof row.metadata === 'string' ? JSON.parse(row.metadata) : (row.metadata || {}) }
catch { return {} }
}
const matches = (body.result || []).filter(row => meta(row).trace_id === trace && meta(row).case === 'bad-input')
console.log(matches.map(row => ({
id: row.id,
provider: row.provider,
model: row.model,
success: row.success,
status_code: row.status_code,
duration: row.duration,
tokens_in: row.tokens_in,
tokens_out: row.tokens_out,
metadata: meta(row)
})))
if (!matches.some(row => row.success === false)) process.exit(2)
NODE
저장한 trace ID, case: "bad-input", Workers AI 공급자 및 실패 상태가 표시되어야 합니다. 생성이 시작되기 전에 잘못된 입력이 실패할 수 있으므로 토큰 수가 비어 있을 수도 있습니다. 아직 항목이 표시되지 않으면 약 20 초 기다린 후 동일한 읽기 전용 블록을 다시 실행합니다.
Dashboard 에서 게이트웨이의 Logs 보기를 엽니다. 메타데이터 필터 또는 표시된 타임스탬프를 사용해 실패한 행을 찾은 다음 세부 정보 패널을 엽니다. 모델, 실패 상태 및 사용자 지정 메타데이터가 동일한 합성 요청을 설명하는지 확인합니다.


게이트웨이 인증 실패와 업스트림 인증 실패 구분
이 단계에서는 한 번에 하나의 자격 증명만 변경합니다. 두 테스트 모두 401 또는 403을 반환할 수 있으므로 상태 코드만으로는 충분하지 않습니다. 로그가 생성된 위치가 부족한 맥락을 제공합니다.
먼저 업스트림 자격 증명은 유효하게 유지하고 게이트웨이 자격 증명만 잘못된 값으로 바꿉니다. 인증된 게이트웨이는 이 요청을 게이트웨이에 진입하기 전에 거부하므로 태그가 지정된 공급자 로그가 생성되지 않습니다.
ACCOUNT_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).accountId')
GATEWAY_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).gatewayId')
TRACE_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).traceId')
MODEL='@cf/meta/llama-3.3-70b-instruct-fp8-fast'
METADATA=$(node -e 'process.stdout.write(JSON.stringify({trace_id:process.argv[1],case:"bad-gateway-auth",synthetic:true}))' "$TRACE_ID")
UPSTREAM_TOKEN=$(npx wrangler auth token --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s).token))')
STATUS=$(curl --http1.1 -sS -o .labex/bad-gateway-auth-response.json -w '%{http_code}' \
-H 'cf-aig-authorization: Bearer deliberately-invalid-gateway' \
-H "Authorization: Bearer $UPSTREAM_TOKEN" \
-H "cf-aig-metadata: $METADATA" \
-H 'Content-Type: application/json' \
--data '{"prompt":"This request must not reach Workers AI.","max_tokens":8}' \
"https://gateway.ai.cloudflare.com/v1/$ACCOUNT_ID/$GATEWAY_ID/workers-ai/$MODEL")
unset UPSTREAM_TOKEN METADATA
printf '%s\n' "$STATUS" | tee .labex/bad-gateway-auth-status.txt
이번에는 게이트웨이 자격 증명은 유효하게 유지하고 업스트림 Workers AI 자격 증명만 바꿉니다. 이 요청은 게이트웨이에 진입하므로 실패한 공급자 기록이 남을 수 있습니다.
METADATA=$(node -e 'process.stdout.write(JSON.stringify({trace_id:process.argv[1],case:"bad-upstream-auth",synthetic:true}))' "$TRACE_ID")
GATEWAY_TOKEN=$(cat .labex/gateway-token)
STATUS=$(curl --http1.1 -sS -o .labex/bad-upstream-auth-response.json -w '%{http_code}' \
-H "cf-aig-authorization: Bearer $GATEWAY_TOKEN" \
-H 'Authorization: Bearer deliberately-invalid-upstream' \
-H "cf-aig-metadata: $METADATA" \
-H 'Content-Type: application/json' \
--data '{"prompt":"This request should reach the upstream authorization check.","max_tokens":8}' \
"https://gateway.ai.cloudflare.com/v1/$ACCOUNT_ID/$GATEWAY_ID/workers-ai/$MODEL")
unset GATEWAY_TOKEN METADATA
printf '%s\n' "$STATUS" | tee .labex/bad-upstream-auth-status.txt
두 요청 모두 401 또는 403을 반환해야 합니다. 잠시 기다린 다음 로그를 새로 생성하지 말고 읽어서 두 태그를 비교합니다.
GATEWAY_TOKEN=$(cat .labex/gateway-token)
curl --http1.1 -fsS \
-H "Authorization: Bearer $GATEWAY_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai-gateway/gateways/$GATEWAY_ID/logs?per_page=50" \
> .labex/logs-after-auth.json
unset GATEWAY_TOKEN
node - <<'NODE'
const rows = require('./.labex/logs-after-auth.json').result || []
const trace = require('./.labex/state.json').traceId
const meta = row => { try { return typeof row.metadata === 'string' ? JSON.parse(row.metadata) : (row.metadata || {}) } catch { return {} } }
for (const name of ['bad-gateway-auth', 'bad-upstream-auth']) {
const found = rows.filter(row => meta(row).trace_id === trace && meta(row).case === name)
console.log(name, found.map(row => ({status_code: row.status_code, success: row.success, provider: row.provider})))
}
NODE
게이트웨이 인증 태그에는 공급자 로그가 없어야 하며, 업스트림 인증 태그에는 실패한 Workers AI 행이 표시되어야 합니다. 따라서 HTTP 상태 코드 하나만 보는 것보다 경계 다이어그램과 상관관계가 있는 로그가 더 많은 정보를 제공합니다.

요청 수정 및 성공 확인
이 단계에서는 두 유효한 자격 증명을 복원하고 필요한 프롬프트를 제공합니다. 런타임 출력과 관측 결과가 일치할 때만 수정이 완료된 것입니다.
같은 trace ID 를 사용하되 새로운 repaired 케이스 이름을 지정합니다.
METADATA=$(node -e 'process.stdout.write(JSON.stringify({trace_id:process.argv[1],case:"repaired",synthetic:true}))' "$TRACE_ID")
GATEWAY_TOKEN=$(cat .labex/gateway-token)
UPSTREAM_TOKEN=$(npx wrangler auth token --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s).token))')
STATUS=$(curl --http1.1 -sS -o .labex/repaired-response.json -w '%{http_code}' \
-H "cf-aig-authorization: Bearer $GATEWAY_TOKEN" \
-H "Authorization: Bearer $UPSTREAM_TOKEN" \
-H "cf-aig-metadata: $METADATA" \
-H 'Content-Type: application/json' \
--data '{"prompt":"In one short sentence, explain why trace IDs help debugging.","max_tokens":48}' \
"https://gateway.ai.cloudflare.com/v1/$ACCOUNT_ID/$GATEWAY_ID/workers-ai/$MODEL")
unset GATEWAY_TOKEN UPSTREAM_TOKEN METADATA
printf '%s\n' "$STATUS" | tee .labex/repaired-status.txt
node -e 'const b=require("./.labex/repaired-response.json"); console.log(b.result?.response ?? b.result)'
HTTP 200과 비어 있지 않은 생성 텍스트가 표시되어야 합니다. 필요하면 로그가 표시될 때까지 기다린 후 이전 단계의 읽기 전용 로그 목록 명령을 다시 실행합니다. Dashboard 에서 trace ID 로 필터링하고 bad-input, bad-upstream-auth, repaired를 비교합니다. 수정된 행에는 성공 상태, 200 상태 코드 및 토큰 사용량이 표시되어야 합니다.

폐기할 게이트웨이 삭제
이 단계에서는 관리 자격 증명이 아직 남아 있는 동안 클라우드 리소스를 삭제하고 삭제 여부를 확인합니다.
ACCOUNT_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).accountId')
GATEWAY_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).gatewayId')
GATEWAY_TOKEN=$(cat .labex/gateway-token)
curl --http1.1 -fsS -X DELETE \
-H "Authorization: Bearer $GATEWAY_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai-gateway/gateways/$GATEWAY_ID" \
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const b=JSON.parse(s);if(!b.success)process.exit(1);console.log("gateway deletion accepted")})'
unset GATEWAY_TOKEN
GATEWAY_TOKEN=$(cat .labex/gateway-token)
curl --http1.1 -fsS \
-H "Authorization: Bearer $GATEWAY_TOKEN" \
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai-gateway/gateways" \
> .labex/gateways-after-delete.json
unset GATEWAY_TOKEN
node -e 'const b=require("./.labex/gateways-after-delete.json"),id=process.argv[1],found=(b.result||[]).some(g=>g.id===id);console.log("gateway absent:",!found);if(found)process.exit(1)' "$GATEWAY_ID"
gateway absent: true가 표시되어야 합니다. 인증된 목록 조회를 사용하면 로그아웃이나 네트워크 오류로 인해 페이지가 표시되지 않은 것과 실제 삭제를 구분할 수 있습니다.
토큰 삭제 및 로그아웃
이 단계에서는 남아 있는 클라우드 자격 증명을 폐기하고 VM 연결을 종료합니다.
Cloudflare Dashboard 에서 My Profile → API Tokens를 엽니다. 저장한 정확한 tokenName을 찾고 Actions를 연 다음 Delete를 선택합니다. 확인 내용을 검토한 후 해당 토큰만 삭제합니다. 게이트웨이 삭제가 이미 확인되었으므로 지금 토큰을 폐기해도 안전합니다.
VM 에 있는 사본을 삭제하고 Wrangler 의 별도 인증을 종료합니다.
shred -u .labex/gateway-token
npx wrangler logout
npx wrangler whoami --json || true
test ! -e .labex/gateway-token && echo "local gateway token removed"
loggedIn: false와 local gateway token removed가 표시되어야 합니다. Dashboard 세션은 별도로 유지되므로 계속 로그인된 상태입니다. 실습이 끝나면 LabEx 가 이 임시 VM 을 저장하지 않고 삭제합니다.
요약
안전한 사용자 지정 메타데이터를 사용해 잘못된 Workers AI 요청을 해당 AI Gateway 로그와 연결했습니다. HTTP 상태 코드에는 경계에 대한 맥락이 필요하다는 점도 확인했습니다. 잘못된 게이트웨이 인증은 공급자 로그가 생성되기 전에 거부되지만, 잘못된 업스트림 인증은 실패한 Workers AI 기록으로 나타납니다. 그런 다음 입력을 수정하고 생성된 텍스트와 성공한 상관 로그를 확인했으며, 폐기할 자격 증명과 리소스를 모두 삭제했습니다.
다음 실습에서는 같은 증거 우선 접근 방식을 캐싱에 적용합니다. 제한된 공개 요청을 한 번 반복하고, 캐시 적중과 새로운 모델 호출을 구분한 다음 최신 출력이 필요할 때 캐시를 우회합니다.



