프롬프트 로깅¶
프롬프트 로깅은 요청과 응답의 텍스트를 로컬 파일에 기록하고, 필요하면 외부 대상으로 내보냅니다. 기본값은 꺼짐입니다. 이 가이드는 프롬프트 로깅을 켤지 판단하고, 켠 뒤에 운영하는 담당자를 위한 문서입니다. 필드별 설명은 프롬프트 로깅 섹션에 있고, 설계 경위는 프롬프트 및 요청 로깅: 설계 기록에 있습니다.
기본값에서 외부로 나가는 정보¶
prompt_logging.enabled: false이거나 prompt_logging 섹션이 없으면, 어떤 요청 경로도 프롬프트나 응답을 읽거나 저장하거나 내보내지 않습니다. 이 기능과 관련된 코드는 실행되지 않으며, 기록 스레드와 캡처 큐, 내보내기 작업도 만들어지지 않습니다.
이 기능이 없을 때 라우터가 기록하는 것은 메타데이터입니다.
- 프로세스 로그와 tracing 스팬: 엔드포인트, 백엔드, 모델, 상태, 지연 시간입니다. credential 헤더는 마스킹되며, 프롬프트나 응답 텍스트는 기록되지 않습니다.
- 사용량 원장: 요청별 토큰 수, 모델, 백엔드, 결과, 지연 시간입니다.
- 허브: 컨트롤 플레인이 연결되어 있으면 사용량 레코드(계수와 귀속 정보)를 받습니다. 레코드 스키마에는 텍스트를 담을 필드가 없습니다.
일부 기능은 요청을 처리하는 데 필요해서 내용을 보관하며, 이들은 요청 로그가 아닙니다. 응답 캐시, 시맨틱 캐시(마지막 사용자 메시지를 임베딩 모델로 전송), Files 및 Batch API가 해당합니다. 프롬프트 로깅은 이 기능들의 동작을 바꾸지 않습니다.
활성화¶
무엇을 어디에 기록할지는 네 가지 설정으로 정해집니다.
enabled: 마스터 스위치입니다.false이면 키가allowed동의를 나타내도 기록하지 않습니다.directory: 레코드 파일을 쓰는 위치이며 기본값은./prompt-logs입니다. 디렉터리가 없으면 라우터가 모드0700으로 만들고(기존 디렉터리의 모드는 그대로 두되, 그룹이나 모든 사용자가 쓸 수 있는 디렉터리면 시작할 때 경고합니다), 각 파일은 모드0600으로 만듭니다. 보존 정책이 디렉터리 안의 일치하는 모든 파일에 적용되므로, 라우터 프로세스 하나가 전용으로 쓰는 디렉터리를 지정하십시오.capture: 레코드에 담을 본문의 범위입니다.prompts,completions,tool_calls의 기본값은 모두true입니다.enabled가true인데 셋을 모두 끄면 로드 오류입니다.- 재시작:
prompt_logging의 모든 필드는 시작할 때 읽습니다. 핫 리로드는 이 섹션을requires_restart로 보고합니다.
프롬프트와 완성은 기록하고 도구 호출은 기록하지 않으며, 파일을 14일 보관하고 패턴 하나를 추가로 마스킹하는 최소 설정입니다.
prompt_logging:
enabled: true
directory: /var/lib/continuum-router/prompt-logs
capture:
prompts: true
completions: true
tool_calls: false
redaction:
patterns:
- 'employee-[0-9]{6}'
retention:
max_files: 16
max_age_days: 14
재시작하기 전에 설정을 검증하십시오.
이 섹션은 "valid": true로 보고됩니다. 디렉터리를 만들거나 쓸 수 없으면 시작이 실패합니다.
기록을 멈추려면 enabled: false로 바꾸고 재시작합니다. 이후에는 기록 스레드와 보존 정책이 실행되지 않으므로, 기존 레코드 파일은 직접 삭제할 때까지 디스크에 남습니다.
동의¶
동의는 어떤 요청을 기록할지 정합니다. enabled를 먼저 확인하고, 다음으로 요청의 키, 마지막으로 default_consent를 봅니다.
enabled: false: 키가 무엇을 나타내든 아무것도 기록하지 않습니다.- 키가
denied를 나타냄: 어느 기본값에서도 요청을 기록하지 않습니다. - 키가
allowed를 나타냄:default_consent: denied에서도 요청을 기록합니다. - 키가 아무것도 나타내지 않거나 요청에 키가 없음:
default_consent가 적용됩니다.allowed(기본값)이면 기록하고denied이면 기록하지 않습니다.
레코드의 consent 필드는 기록 근거를 key_allowed 또는 deployment_default로 나타냅니다.
키의 동의가 오는 곳¶
키는 다음 두 곳 중 하나에서 동의를 나타냅니다.
- 로컬에 설정한 키: 키의
prompt_logging_consent입니다.api_keys.api_keys[], 외부 키 파일, Admin API, WebUI에서 지정합니다. 값은allowed또는denied입니다. 이 필드가 없는 키는 동의를 나타내지 않습니다. 허브가 필요 없으므로 허브 없는 배포에서도default_consent: denied로 옵트인 캡처를 운영하거나,default_consent: allowed에서 특정 키만 제외할 수 있습니다. - 허브에서 동기화한 키: Continuum Hub가 라우터에 동기화하는 키 테이블입니다. 허브는 테이블을 만들 때 팀의 동의를 각 키의 값에 합성합니다. 라우터에는 팀 동의 설정이 없습니다.
prompt_logging:
enabled: true
default_consent: denied
api_keys:
api_keys:
- key: "${RESEARCH_KEY}"
id: research
user_id: research
organization_id: acme
prompt_logging_consent: allowed
토큰이 두 테이블에 모두 있으면 값은 다음 순서로 결합됩니다.
- 로컬 키 또는 허브 키의
denied - 로컬
allowed - 허브
allowed default_consent
실패 시 차단 구간은 이 모두보다 우선하며, 로컬 allowed도 예외가 아닙니다.
로컬 값은 인증된 키에 적용됩니다. permissive와 blocking 모드 모두, Authorization: Bearer와 x-api-key 모두 해당합니다. 인증되지 않은 제시 토큰에는 로컬 값이 적용되지 않습니다. blocking 모드는 토큰이 허브 키이기도 한 경우가 아니면 이 요청을 거부하고, permissive 모드는 키가 없는 요청으로 처리합니다.
나머지 경우는 다음과 같습니다.
denied를 지정한 비활성화 또는 만료 키: 두 모드 모두 캡처하지 않으므로, 키를 비활성화해도 동의 범위가 넓어지지 않습니다.- 허브 정책 저장소가 없고 제시 토큰이 인증되지 않음:
default_consent와 관계없이 캡처하지 않습니다.allowed를 지정했거나 값이 없는 비활성화·만료 키는 아무것도 허용하지 않으며, 삭제하거나 교체한 키의 이전 값이나 알 수 없는 토큰도 캡처하지 않습니다. permissive라우터에 보낸 자리 표시자(placeholder) 토큰: 캡처하지 않습니다.default_consent를 따르려면 토큰을 보내지 않아야 합니다.- 허브 정책 저장소가 있고 어느 테이블도 모르는 토큰: 캡처하지 않으며, 테이블이 허용한 키의 명시된 동의만 적용됩니다.
api_keys_file이 없거나 읽을 수 없음, 또는persistence_file을 읽을 수 없음: 라우터는 경고를 기록하고 그대로 시작합니다. 재시작으로 파일을 불러올 때까지 로컬 검증에 실패한 토큰은 허브 키를 포함해 캡처하지 않습니다.api_keys_file을 읽을 수 없으면api_keys[]도 불러오지 않습니다. 읽을 수 없는persistence_file은 그대로 유지되며, 재시작 전까지 런타임 키는 메모리에만 있습니다.- 키가 없는 요청: 계속
default_consent를 따릅니다.
변경 사항은 다른 api_keys 변경과 같은 방식으로 반영됩니다. 파일 감시기를 통한 파일 키 변경은 재시작이 필요하고, Admin API 변경은 다음에 입장하는 요청부터 적용됩니다. 파일 키에 대한 Admin API 변경은 재시작 전까지만 유지되고 재시작하면 파일의 값으로 돌아가므로, 동의를 철회할 때는 파일도 함께 수정하십시오. 이미 진행 중인 요청은 입장 시점에 읽은 동의를 유지합니다.
Note: continuum-hub#1994 이후의 허브는 키 테이블로 키별 동의를 전달합니다. 그보다 오래된 허브는 동의를 전달하지 않으므로, 허브에서 동기화한 키는
default_consent를 따릅니다.default_consent: denied에서 이런 허브를 쓰면allowed를 나타낸 로컬 키만 기록됩니다.
실패 시 차단 구간¶
키의 denied는 이 라우터가 동의를 읽는다는 사실을 허브가 알고 만든 키 테이블에서만 읽을 수 있습니다. 그런 테이블이 효력을 갖기 전까지는 default_consent 값과 관계없이 키로 인증된 요청을 기록하지 않습니다. 키가 없는 요청은 계속 default_consent를 따르고, 허브 정책 저장소가 없는 배포는 영향을 받지 않습니다. 해당 경우는 다음과 같습니다.
- 아직 정책이 없음: 첫 동기화 전이거나 복원된 키 테이블이 만료된 뒤에는 저장소에 테이블이 전혀 없습니다.
- 허브가 아직 capability를 저장하지 않음: 허브는 저장된 capability에
prompt_logging_consent_v1이 있는 라우터에만 동의를 전달하며, 라우터는 이 capability를 하트비트로 광고합니다. 허브가 하트비트를 받아들이기 전에 요청한 테이블은 capability 없이 만들어졌을 수 있으므로, 라우터는 그 테이블에 동의가 없다고 봅니다. 첫 하트비트는 시작 직후 보내고 승인되는 즉시 정책을 다시 요청하므로, 이 구간은 보통 1초 안에 끝납니다. - 하트비트가 계속 실패함: 구간이 끝나지 않으며, 라우터는 첫 실패 시 결과를 알리는
warn로그를 한 번 남깁니다. - 오래된 허브: continuum-hub#1994 이전의 허브는 하트비트를 받아들이고도 동의를 보내지 않습니다. 다음 테이블에서 구간이 끝나고, 허브에서 동기화한 키는
default_consent를 따릅니다. 구간이 처음 끝날 때 라우터는 동의를 나타내는 키 수를 담은info로그를 한 번 남깁니다. - 복원된 키 테이블: 키 테이블 파일(
persist_key_table)에서 복원한 테이블은 동의가 있는 테이블에서 쓴 파일일 때만 동의를 가지며, 파일에는consent_confirmed로 기록됩니다. 이전 라우터 빌드가 쓴 파일은 동의가 없는 테이블로 복원되고, 허브의 다음 테이블이 올 때까지 구간이 유지됩니다. unmatched_key: fail_open: 어느 테이블도 모르는 토큰을 제시한 요청은 기록하지 않습니다. 로컬 인증은 통과했지만 허브 테이블에 없는 키는 로컬 값과default_consent를 따르므로, 두 테이블에 모두 있는 키의 허브 행이 아직 동기화되지 않았다면 최대 한 폴링 간격 동안 로컬 값을 따릅니다.
이 상태는 요청을 받아들이는 시점에 고정됩니다. 오래 걸리는 요청 도중에 동기화가 끝나도 그 요청이 기록 대상으로 바뀌지는 않습니다.
레코드의 내용¶
기록되는 요청마다 directory 아래 prompt-log-<sequence>.jsonl 파일에 JSON 한 줄이 쓰입니다.
{
"schema_version": 2,
"request_id": "req-0f3a",
"created_at_ms": 1785000000000,
"written_at_ms": 1785000002113,
"model": "gpt-5.6-sol",
"provider": "openai",
"key_id": "team-a",
"consent": "deployment_default",
"prompt": "user: Summarize the attached report.",
"completion": "The report covers ...",
"usage": { "input_tokens": 412, "output_tokens": 96 },
"outcome": "ok"
}
- 귀속 정보:
request_id,model(클라이언트가 요청한 이름),provider(응답한 백엔드이며 백엔드에 도달하지 못했으면 없음),key_id(키의 ID이며 키 값이 아님)입니다. - 내용:
prompt,completion,tool_calls입니다. 해당capture범위가 켜져 있고 요청이 그 내용을 만들었을 때만 들어갑니다. 이미지 같은 텍스트가 아닌 부분은 자리 표시자로 기록하며, 추론(reasoning) 출력은 기록하지 않습니다. outcome:ok,refused(가드레일이 요청이나 응답을 차단),backend_error,client_cancelled,stream_errored중 하나입니다.ok가 아닌 결과에는 짧은error요약이 붙을 수 있으며, 이 요약도 마스킹됩니다.truncated: 전체를 담지 못한 필드의 이름입니다. 필드는 256 KiB에서 잘리고, 도중에 끝난 스트림은 끝난 지점까지만 담으며, 마스킹은 필드당 최대 1 MiB를 남깁니다.
스트리밍 응답은 스트림이 끝날 때 한 번 기록합니다. 폴백한 요청은 응답한 홉의 레코드 하나만 남깁니다.
마스킹¶
모든 레코드는 큐에 넣기 전에 요청 태스크에서 한 번 마스킹됩니다. 마스킹되지 않은 텍스트는 파일, 로그 줄, 오류 메시지 어디에도 기록되지 않습니다. 마스킹은 다음 순서로 두 층이 적용됩니다.
- 내장 credential 패턴: OpenAI와 Anthropic 형식의
sk-키, GitHub 토큰(ghp_등 관련 접두사와github_pat_), AWS 액세스 키 ID, GoogleAIza키, Slackxox토큰, JWT, PEM 개인 키 블록,Bearercredential입니다. redaction.patterns: 직접 추가하는 정규식입니다. 일치한 부분은redaction.masked_replacement(기본값[redacted])로 바뀌며, 이 값은 문자열 그대로 삽입됩니다. 올바르지 않거나 빈 문자열과 일치하는 패턴은 로드 오류이고, 오류 메시지는 패턴이 아니라 항목의 인덱스를 알려 줍니다.
마스킹 대상 필드는 model, prompt, completion, 각 도구 호출, error입니다.
내보낼 때의 마스킹¶
내보내기는 레코드를 보내기 전에 현재 설정으로 다시 마스킹합니다. 캡처한 뒤에 추가한 패턴도 이전 파일에 적용됩니다. 위의 필드에 더해 request_id, provider, key_id도 마스킹합니다.
Warning: 마스킹은 패턴 일치 방식입니다. 패턴과 비슷해 보이는 텍스트만 가리며, 그 밖의 정보는 가리지 않습니다. 개인정보, 내부 이름, 어떤 패턴에도 맞지 않는 형식의 credential은 입력된 그대로 기록됩니다. 레코드 파일은 민감한 파일로 취급하고, 외부로 내보내기 전에 자체 트래픽에 맞춰 패턴을 검토하십시오.
보존¶
로컬 파일은 세 설정으로 제한되며, 모두 1 이상이어야 합니다. 0은 무제한으로 해석되지 않고 거부됩니다.
| 필드 | 기본값 | 효과 |
|---|---|---|
retention.max_bytes | 100 MiB | 파일을 순환하는 크기입니다. |
retention.max_files | 10 | 순환할 때마다 유지하는 파일 수입니다. |
retention.max_age_days | 7 | 라우터가 실행되는 동안 레코드가 디스크에 남는 최대 기간입니다. |
기간 제한은 활성 파일의 첫 레코드가 max_age_days의 8분의 1만큼 오래되면 파일을 새로 시작하고, 어떤 레코드도 제한을 넘기지 않도록 파일을 일찍 삭제하는 방식으로 적용됩니다. 그래서 트래픽이 있으면 max_age_days마다 파일이 약 여덟 개 생기며, 기간이 적용되는 제한이 되려면 max_files를 9 이상으로 설정해야 합니다. 9 미만이면 파일 수 제한이 기간보다 먼저 레코드를 지울 수 있습니다. 시점 규칙은 보존 기간에 있습니다.
보존 정책은 라우터가 실행되는 동안에만 동작합니다. 보존 정책은 내보내기 싱크를 기다리지도 않습니다. 싱크가 읽기 전에 파일이 삭제되면 그 파일은 건너뛰고 pruned로 집계합니다.
기록 스레드가 지연될 때¶
전용 스레드가 레코드를 쓰므로 디스크가 느려도 요청은 지연되지 않습니다. 요청 경로는 레코드를 크기 256의 큐에 넘깁니다. 큐가 가득 찼거나 쓰기가 실패하면(디스크 가득 참, 디렉터리 삭제) 그 레코드는 버려지고 요청은 영향을 받지 않습니다. 두 카운터가 이를 보고합니다.
prompt_log_records_total{result="queue_full"}: 큐가 가득 찼습니다.prompt_log_records_total{result="sink_error"}: 쓰기가 실패했으며, 내용 없는ERROR로그 한 줄이 남습니다.
버려진 레코드는 다시 시도하지 않으며 어떤 싱크로도 전달되지 않습니다.
내보내기¶
싱크를 설정하지 않으면 레코드는 보존 정책이 삭제할 때까지 로컬 파일에 머뭅니다. 싱크는 레코드를 다른 곳으로 전달합니다. 싱크는 enabled가 true일 때만 실행되며, 각각 자체 작업과 커서를 가지므로 실패하는 대상 하나는 그 싱크만 지연시킵니다. 싱크 필드 전체 표는 내보내기 싱크에 있습니다.
prompt_logging:
enabled: true
export:
sinks:
- type: webhook
endpoint: "https://logs.example.com/ingest"
headers_env:
Authorization: COLLECTOR_AUTH
- type: langfuse
endpoint: "https://langfuse.example.com"
- type: s3
bucket: prompt-logs
prefix: router-1/
region: us-east-1
대상별 동작¶
webhook: 레코드의 JSON 배열을POST합니다. credential은 헤더 이름을 환경 변수에 대응시키는headers_env로 지정합니다. 이 필드는 라우터 프로세스가 읽을 수 있는 어떤 환경 변수든 지정할 수 있고, 그 값은 배치마다 엔드포인트로 전송됩니다. 따라서 Admin API를 포함해 이 섹션을 편집할 수 있는 사람은, 자신이 제어하는 수집기를 통해 라우터의 환경 변수를 읽을 수 있습니다. 라우터의 환경 변수는 필요한 것만 두십시오.langfuse:{endpoint}/api/public/otel/v1/traces로 OTLP JSON을POST하며,public_key_env와secret_key_env로 Basic 인증을 합니다. 레코드 하나가generation스팬 하나가 됩니다. Langfuse v4는 두 번 받은 스팬을 안정적으로 중복 제거하지 않으므로, 다시 전달된 레코드가 Langfuse에 두 번 나타날 수 있습니다.s3: 배치당 JSONL 객체 하나이며 이름은<prefix>/prompt-log-<sequence>-<offset>.jsonl입니다.prompt-log-s3빌드 기능이 필요하고, 공식 릴리스 바이너리에는 포함되어 있습니다. 이 기능 없이 빌드한 바이너리는 설정을 불러오되 싱크가 동작하지 않는다는 경고를 남기고 아무것도 보내지 않습니다.
모든 credential은 환경 변수 이름이며 시작할 때 한 번 읽습니다. 환경 변수가 설정되지 않았거나 비어 있으면 라우터가 시작되지 않고, config validate가 먼저 경고합니다.
전달 보장¶
전달은 최소 한 번(at-least-once)입니다. 싱크는 directory 아래 .export-cursor-<id>.json에 커서(파일 시퀀스와 바이트 오프셋)를 모드 0600으로 저장하고, 배치를 전달한 뒤에 커서를 옮깁니다. 크래시나 재시작이 있으면 처리 중이던 배치가 다시 전달될 수 있으므로, 수신 측은 중복을 허용해야 합니다.
새 싱크가 시작하는 위치는 다음과 같습니다.
backfill: false(기본값): 보존된 레코드의 끝에서 시작하고, 시작한 뒤에 쓰인 레코드만 보냅니다. 이전 레코드는 명시적으로 선택해야만 새 대상에 전달됩니다.backfill: true: 보존 정책이 아직 가지고 있는 모든 레코드를 먼저 보냅니다.
커서가 이미 있으면 backfill은 효과가 없습니다. 커서 파일을 삭제하면 그 싱크는 다시 새 싱크가 됩니다. 웹훅 URL의 토큰을 포함해 싱크의 대상을 바꿔도 새 커서를 가진 새 싱크가 됩니다. 단, cursor_id를 설정한 싱크는 예외입니다.
토큰을 교체해도 커서 유지하기¶
cursor_id는 대상의 해시 대신 커서 파일 이름(.export-cursor-<type>-<cursor_id>.json)을 정합니다. 따라서 웹훅 URL의 토큰을 교체해도 위치가 유지되고, 아직 전달하지 않은 레코드도 계속 전달됩니다.
- 형식: 영문자, 숫자,
_,-로 이루어진 1~64자입니다. 같은 유형의 싱크 두 개가 대소문자만 다른 값을 포함해 같은 값을 쓸 수 없으며, 어느 쪽이든 어기면 로드 오류입니다. - 기존 싱크에 추가할 때: 싱크는 한 번 새 커서로 시작하며
backfill을 따릅니다.backfill이true가 아니면 이전 커서가 아직 도달하지 못한 레코드는 보내지 않습니다.
Warning:
cursor_id를 설정하면 싱크는 아직 전달하지 않은 레코드를 지금 지정된 대상으로 보냅니다. 이 레코드를 받은 적 없는 호스트도 마찬가지입니다. 대상을 바꾸기 전에 새 대상이 이 레코드를 받아도 되는지 확인하십시오.
전달이 실패할 때의 동작은 다음과 같습니다.
- 전달 오류: 배치는 디스크에 남고,
interval_seconds에서max_backoff_seconds까지 지수 백오프로 다시 시도합니다. 시도마다failed로 집계하고, 첫 실패에서 경고를 한 번 남깁니다. - 영구 거부: 대상이 거부하는 레코드를 참고하십시오.
- 파싱할 수 없는 레코드: 레코드가 아니거나 너무 길거나 끊긴 줄은 건너뛰고
malformed로 집계합니다. 싱크를 막지 않습니다. - 읽을 수 없는 커서 파일(
EACCES같은 I/O 오류): 싱크는interval_seconds마다 다시 시도하며 그동안 아무것도 보내지 않고, 가장 오래된 파일이나 파일 끝으로 이동하지도 않습니다. 유효한 커서가 없는 커서 파일은 커서가 없는 것으로 취급하며backfill을 따릅니다. - 시작: 시작 시 읽을 수 없는 커서를 참고하십시오. 유효한 커서가 없는 파일은 시작에 영향을 주지 않습니다.
대상이 거부하는 레코드¶
기본값(on_permanent_failure: block)에서는 거부된 배치를 모두 다시 시도합니다. 따라서 대상이 절대 받지 않을 레코드가 있으면, 원인을 고치거나 보존 정책이 해당 파일을 삭제할 때까지 그 싱크가 멈춥니다. Langfuse가 partialSuccess로 거부했다고 보고한 스팬은 예외이며, 항상 rejected로 집계하고 넘어갑니다.
on_permanent_failure: skip을 설정하면, 해당하는 상태로 거부된 배치를 포기합니다.
- 해당: 레코드나 배치 페이로드 문제를 나타낼 수 있는 400, 409, 413, 415, 422입니다.
- 항상 재시도: 인증, 프록시, 경로, 라우팅, 속도 제한, 서버 장애를 포함한 나머지 모든 상태입니다. 타임아웃이나 연결 거부처럼 상태가 없는 실패도 재시도합니다.
- S3 오류 코드:
s3싱크에서 응답 본문에 S3 오류 코드가 있는 400이나 409는, 코드가EntityTooLarge가 아니면 재시도합니다. S3는 객체를 내용과 무관한 바이트로 저장합니다. 따라서 만료된 세션 토큰(ExpiredToken), 잘못된 리전, 지연된 업로드,OperationAborted는 레코드 문제로 보지 않습니다. - 집계: 거부 전에 대상이 받은 레코드는
delivered로 남습니다. 배치의 나머지는 아직 보내지 않은 레코드까지 포함해dropped로 집계하고, 싱크, 상태, 개수를 담은 경고를 한 번 남깁니다. 어떤 레코드가 거부를 일으켰는지 라우터가 알 수 없으므로 배치 단위로 처리합니다.
Warning:
skip은 레코드를 잃습니다. 오동작하는 대상이 있으면 문제 레코드 하나보다 훨씬 많이 버려질 수 있습니다. 잘못된 자격 증명에 400으로 응답하는 게이트웨이는 거부로 처리되고, 싱크는 백오프 없이 배치를 하나씩 포기하므로 백로그가 빠르게 버려질 수 있습니다.dropped에 알림을 설정하십시오.
시작 시 읽을 수 없는 커서¶
첫 레코드 파일 번호는 모든 커서보다 커야 합니다. 그렇지 않으면 싱크가 새 파일을 이미 읽은 파일로 판단해 새 레코드를 건너뜁니다. 그래서 커서를 읽을 수 없는 동안 라우터는 레코드를 쓰지 않으며, 관측 경로를 잃더라도 트래픽은 멈추지 않습니다.
- 라우터는 200ms 간격으로 커서를 최대 5번 다시 읽습니다.
- 그래도 실패하면 오류 종류를 담은
ERROR로그를 한 번 남기고,prompt_log_capture_disabled{reason="cursor_unreadable"}을 1로 설정합니다. - 다음 시작 때까지 캡처와 내보내기 태스크 없이 요청을 처리합니다.
복구하려면 파일을 고친 뒤 재시작하십시오. 읽거나 만들 수 없는 디렉터리는 여전히 시작을 중단시킵니다.
Note: 이렇게 캡처가 꺼져 있어도 보존 정책은 계속 동작합니다. 보존 전용 타이머가 레코드 파일을 열거나 만들지 않고
max_age_days와max_files를 적용합니다.
동의 철회¶
싱크는 레코드를 전달하기 전에 레코드의 키를 다시 확인합니다. 따라서 동의를 철회하면 이후의 캡처와 아직 이루어지지 않은 내보내기가 모두 멈춥니다. 백필하는 싱크도 같은 규칙을 따릅니다. 각 레코드는 그 시점의 키 동의로 판단합니다.
| 현재 키 상태 | 싱크의 동작 | 집계 |
|---|---|---|
| 레코드에 키가 없음 | 전달 | delivered |
| 동의를 읽을 수 없음(실패 시 차단 구간) | 보류: 이 레코드에서 멈추고 커서를 더 옮기지 않음 | 집계하지 않음 |
denied | 건너뛰고 지나감 | consent_withdrawn |
allowed | 전달 | delivered |
| 아무것도 지정하지 않음 | default_consent를 따름: allowed면 전달, denied면 건너뜀 | delivered 또는 consent_withdrawn |
| 어느 테이블에도 키가 없음 | 건너뛰고 지나감. 제거된 키는 동의를 지정할 수 없음 | consent_key_absent |
보류 중에 싱크는 해당 레코드 앞의 레코드를 전달하고 info 로그를 한 번 남긴 뒤, interval_seconds마다 다시 시도합니다. 보류가 풀리면 로그를 한 번 더 남기며, 백로그 게이지는 계속 보고됩니다.
재확인에는 다음과 같은 한계가 있습니다.
- 이미 전달한 레코드: 회수할 수 없습니다. 필터와 전달 사이에는 다시 확인하지 않으므로, 동의가 바뀌는 시점에 배치 하나가 전송 중일 수 있습니다.
- 로컬 파일: 다시 쓰지 않습니다. 철회한 키의 레코드는 보존 정책이 삭제할 때까지 디스크에 남습니다.
- 키 제거: 로컬 테이블과 허브 테이블에서 제거한 키는
consent_key_absent로 집계하며, 아직 전달하지 않은 레코드를 보내지 않습니다. 로컬 키를 제거하지 않고 철회하려면 해당 키의 설정이나 Admin API, WebUI에서denied를 지정하십시오. 이 경우consent_withdrawn으로 집계합니다. - 로드에 실패한 키 소스: 없거나 읽을 수 없는
api_keys_file또는persistence_file에 키의denied가 있었을 수 있으므로, 파일을 고치고 라우터를 재시작할 때까지 키가 있는 모든 레코드를 보류합니다. 요청은 계속 처리하며, 백로그 게이지는 계속 증가합니다. - 폐기된 허브 키: 키 제거만 부재에 의한 철회입니다. 폐기, 비활성화, 만료된 허브 키는 테이블에 남아 있으므로 지정한 동의가 계속 적용되고, 아무것도 지정하지 않은 키는
default_consent를 따릅니다. 이런 키의 내보내기를 멈추려면 허브에서 해당 키에denied를 지정하세요. - AppProxy ROUTER 키: 동의를 지정하지 않으므로, 라우터가 켜져 있고 해당 테이블에 키가 있는 동안 레코드는
default_consent를 따릅니다. 그 테이블에서 제거한 키는consent_key_absent로 집계하며, 라우터가 첫 키 스냅샷을 받기 전까지 이런 키의 레코드는 보류합니다. 라우터가 꺼져 있으면 이 레코드는consent_key_absent로 집계합니다. - 마스킹된 키 ID: 동의는 redactor가 마스킹하기 전, 캡처된 키 ID로 판단합니다. 따라서
redaction.patterns항목이나,sk-뒤에 토큰 문자가 10자 이상 이어지는 형태 같은 기본 패턴이 키 ID와 일치해도 테이블에서 키를 가릴 수 없습니다. 레코드에 기록되거나 전송되는 ID는 여전히 마스킹된 값뿐입니다. 허브 푸시는 예외입니다. 허브는 ID를 자체api_keys테이블과 대조하며[A-Za-z0-9._-]{1,128}형식만 받으므로, 마스킹으로 ID가 바뀐 레코드는 로컬에서 거부되어POST없이rejected로 집계되고 커서가 지나갑니다. 프로세스당 한 번, ID 없이,redaction패턴이 허브 키 ID와 일치해 해당 키의 레코드를 푸시할 수 없다는warn이 기록됩니다. 키의 레코드가 예상과 달리 건너뛰어지면redaction패턴이 해당 키 ID와 일치하는지 확인하세요.
Warning: 내보내기는
capture범위를 다시 확인하지 않습니다.capture를 좁힌 뒤나 백필하는 싱크에서, 더 넓은 범위로 기록된 백로그는 보존 정책이 삭제할 때까지 계속 전달됩니다. 이 전달을 멈추려면export.sinks에서 해당 싱크를 제거하십시오.
네트워크 규칙¶
- 리다이렉트 없음:
3xx응답은 실패로 처리하며 따라가지 않으므로, 내용이 다른 호스트로 넘어가지 않습니다. - 사설 주소: 사설, 루프백, 링크 로컬, 통신사급 NAT(
100.64.0.0/10), 벤치마킹, 멀티캐스트, 메타데이터 주소의endpoint는 전달할 때마다 거부됩니다. 일반 표기, IPv4 매핑, NAT64, 6to4, Teredo 표기에 모두 적용됩니다. 네트워크 내부의 수집기에는allow_private_endpoint: true를 설정하십시오. - 이름 확인: 엔드포인트의 호스트 이름은 전달할 때마다 확인하며, 확인되지 않는 이름은 전달이 실패합니다. 연결은 검사를 통과한 주소로만 이루어지므로, 검사 뒤에 응답이 바뀌는 이름이 전달을 다른 곳으로 돌릴 수 없습니다.
- S3 환경 변수 엔드포인트:
AWS_ENDPOINT_URL_STS,AWS_METADATA_ENDPOINT,AWS_CONTAINER_CREDENTIALS_FULL_URI는 전달 전에 같은 검사를 받으며, S3 대상과 검사된 주소 테이블을 공유합니다. 호스트 이름을 확인할 수 없어도 라우터 시작은 중단되지 않습니다. 이름 확인이 복구될 때까지 해당 싱크가 멈추고failed가 증가합니다. ECS와 EKS Pod Identity의 고정 호스트인169.254.170.2,169.254.170.23,[fd00:ec2::23]은 예외입니다. 그 밖의 사설 또는 루프백 자격 증명 호스트에는allow_private_endpoint: true가 필요하며, 이 설정은 주소 검사를 면제합니다. - S3 커서 식별자:
AWS_ENDPOINT_URL_STS나AWS_METADATA_ENDPOINT가 바뀌면 싱크는 새 커서를 갖습니다.AWS_CONTAINER_CREDENTIALS_FULL_URI는 대상이 아니라 자격 증명 소스이므로 커서를 바꾸지 않습니다. - S3 대상 환경 변수:
AWS_ENDPOINT,AWS_ENDPOINT_URL,AWS_ENDPOINT_URL_S3중 하나가 바뀌어도 싱크는 새 커서를 갖습니다. - 이그레스 프록시:
HTTP_PROXY나HTTPS_PROXY를 쓰면 프록시가 대상과 S3 환경 변수 엔드포인트의 이름을 확인하고 주소를 고릅니다. 따라서 위 주소 제한은 해당 연결에 적용되지 않으므로, 이그레스 정책은 프록시에서 적용하세요. 검사는 그대로 먼저 실행됩니다. 라우터가 각 이름을 직접 확인할 수 없으면 해당 싱크가 멈추고failed가 증가합니다. 이런 환경에서는allow_private_endpoint: true로 주소 검사를 면제할 수 있습니다. - 평문 전송: 사설로 보이지 않는 호스트의
http://엔드포인트는 내용과 credential이 암호화되지 않은 채 전송되므로config validate와 시작할 때 경고합니다. 싱크는 그대로 실행됩니다.https://를 사용하십시오.
허브 푸시¶
export.hub_push: true는 캡처한 레코드를 라우터가 등록된 Continuum Hub의 POST /api/agent/v1/prompt-logs로 등록 자격 증명과 함께 보냅니다. 본문이 허브에 도달하는 유일한 채널이며, 사용량 레코드는 계속 메타데이터 전용입니다. 동의를 허브의 키 테이블에서 읽으므로 control-plane 기능으로 빌드한 바이너리와 control_plane.policy.enabled: true가 필요합니다. 그렇지 않으면 이 설정은 경고를 남기고 아무것도 보내지 않습니다.
prompt_logging:
enabled: true
export:
hub_push: true
control_plane:
enabled: true
hub_url: "https://hub.example.com"
enrollment_token: "${CONTINUUM_HUB_ENROLLMENT_TOKEN}"
policy:
enabled: true
허브 푸시는 내보내기 루프의 작업 하나를 더 실행합니다. 자체 커서(.export-cursor-hub-<hash>.json, 해시는 허브 URL과 라우터 ID를 포함), 읽을 때의 동일한 마스킹, sink="hub" 레이블의 메트릭, 최소 한 번 전달을 그대로 씁니다. 싱크와 다른 점은 다음과 같습니다.
- 동의: 레코드는 키가 허브의 키 테이블에
allowed로 있고,default_consent가 아니라 그allowed를 근거로 캡처되었으며, 로컬denied가 이를 덮지 않을 때만 나갑니다. 키 없는 레코드, 배포 기본값으로 캡처한 레코드, 로컬 테이블에만 있는 키, 동의를 지정하지 않은 키의 레코드는 나가지 않으며, 각각consent_keyless,consent_deployment_default,consent_key_absent,consent_withdrawn으로 집계합니다. 키의 동의를 아직 읽을 수 없는 레코드는 싱크와 마찬가지로 푸시를 보류시킵니다. - 시작 위치: 커서가 없는 라우터는 레코드 파일의 끝에서 시작하며,
backfill은 없습니다. 재등록하거나 다른 허브에 연결하면 그 시점의 끝에서 시작합니다. - 전송 형식: 레코드는
request_id,key_id,completed_at_ms, 각각 64 KiB로 자른 마스킹된prompt와completion, 그리고 멱등 키<router_id>:<request_id>를 담습니다. tool call, 사용량, 모델, 결과는 나가지 않습니다. 요청 하나는 허브의prompt_log_batch_max_records(기본 100)개 레코드와 16 MiB의 인코딩된 JSON을 넘지 않으며, 푸시는usage_push_interval_secs마다 실행됩니다. - 허브 응답:
404는 응답 본문이 수집 핸들러 자신의{"error":"prompt-log ingest disabled"}일 때만 밀린 레코드를 버립니다. 프록시나 잘못된 경로가 보내는 다른404는 재시도하므로 레코드는 디스크에 남습니다. 수집이 꺼진 것으로 확인되면 커서를 파일 끝으로 옮기고, 건너뛴 레코드는dropped로 집계합니다. 이 값은 엄격한 동의 필터가 건너뛰었을 수 있는 줄까지 포함하므로 상한입니다.413은 그 요청 전체를 버립니다.400,403,409는 레코드를 하나씩 다시 보내되 단일 레코드 요청 사이에 짧게 쉬므로, 철회된 키 하나는 자기 레코드만 거부되게 합니다. 배치 전체가 거부되면 푸시는 한 주기 동안 멈춥니다.401,408,429,5xx, 전송 실패는 최대 60초의 백오프 뒤 같은 레코드를 재시도합니다.3xx는 따라가지 않고 재시도하며, 프로세스당 한 번 상태 코드와 함께hub_url이 최종 위치를 가리켜야 한다고 경고합니다. 그 밖의4xx는rejected로 집계합니다. 허브는 멱등 키로 중복을 제거하므로 크래시 뒤의 재전달은 행을 추가하지 않으며, 허브가 이미 저장했다고 답한 레코드는duplicate로 집계합니다. - 운영자 한계: 수집이 꺼진 채널은 밀린 레코드 전체를 영구히 버리므로, 오래된 본문이 나가지 않기를 원하면 푸시를 켜기 전에 허브의 수집을 먼저 켜십시오. 허브가 폐기한 라우터는 모든 레코드가 하나씩 거부되어 각각
rejected로 집계되며, 라우터를 제거하거나 다시 등록할 때까지 계속됩니다. 배치 전체가 거부된 뒤의 일시 정지는 그 백로그가 사라지는 속도를 제한할 뿐 막지는 못합니다. 팀의denied는 라우터 설정이 아니라 허브의 키 테이블을 통해서만 도착하며, 각 키의 동의에 합쳐져 전달됩니다. continuum-hub#1994 이전의 허브는 이를 전달하지 않습니다. - 평문 전송: 루프백이 아닌 호스트의
http://hub_url은 프롬프트 내용과 credential이 암호화되지 않은 채 전송되므로 설정을 불러올 때 거부됩니다.https://를 사용하십시오. - 로컬 파일: 푸시한 레코드를 레코드 파일에서 지우지 않습니다. 모든 싱크와 같이
retention이 파일을 제한합니다.
기록되는 요청¶
| 경로 | 기록 여부 |
|---|---|
POST /v1/chat/completions(채팅으로 연결되는 A2A 브리지 포함) | 기록함 |
POST /v1/responses | 기록함 |
POST /anthropic/v1/messages | 기록함 |
POST /v1/completions | 기록하지 않음 |
| 임베딩 | 기록하지 않음 |
| ACP | 기록하지 않음 |
다음 경우에도 요청을 기록하지 않습니다.
- 응답 캐시 적중: 캐시된 응답은 레코드 없이 제공됩니다.
- 검증이나 요청 정책으로 디스패치 전에 거부된 요청: 레코드를 쓰지 않습니다.
- 동의 단계를 통과하지 못한 요청: 동의를 참고하십시오.
- 버퍼링 요청을 클라이언트가 취소한 경우: 라우터가 내부 스트림을 읽는 도중에 취소한 경우입니다.
입력 가드레일이 차단한 요청은 프롬프트만 있고 응답이 없는 refused 레코드로 기록됩니다.
아직 사용할 수 없는 기능¶
- 허브 열람: 허브는 푸시된 레코드를 저장하지만(continuum-hub#1622) 아직 표시하지는 않습니다(continuum-hub#1623).
- 오래된 허브의 키별 동의: lablup/continuum-hub#1994보다 오래된 허브는 동의를 전달하지 않으므로, 허브에서 동기화한 키는
default_consent를 따릅니다. 키의 동의가 오는 곳을 참고하십시오.
메트릭과 문제 해결¶
| 메트릭 | 의미 |
|---|---|
prompt_log_records_total{result} | 캡처 경로가 만든 레코드 수이며 written, queue_full, sink_error로 나뉩니다. |
prompt_log_export_total{sink,result} | 싱크별 결과별 레코드 수이며 delivered, rejected, failed, dropped, malformed, pruned, consent_withdrawn, consent_key_absent로 나뉩니다. 허브 푸시에는 consent_keyless, consent_deployment_default, duplicate가 더 있습니다. 허브 푸시의 dropped는 수집이 꺼진 채널이 건너뛴 읽지 않은 백로그도 포함하며, 엄격한 동의 필터가 건너뛰었을 수 있는 줄까지 세므로 상한입니다. |
prompt_log_export_backlog_bytes{sink} | 싱크가 아직 전달하지 않은 디스크의 레코드 바이트 수입니다. |
prompt_log_capture_disabled{reason} | 읽을 수 없는 커서 때문에 이 프로세스의 캡처와 내보내기가 꺼졌으면 1입니다. reason은 cursor_unreadable입니다. 그 외에는 시리즈가 없습니다. |
sink는 싱크 유형이며, 같은 유형의 싱크가 더 있으면 -1, -2 같은 접미사가 붙습니다. 허브 푸시는 hub입니다.
| 증상 | 확인할 것 |
|---|---|
| 레코드 파일이 생기지 않음 | enabled가 true이고 라우터를 재시작했는지 확인합니다. 요청이 기록되는 경로로 들어왔고 캐시 적중이 아니며 동의를 통과했는지도 확인합니다. 정책이 아직 없거나 허브가 하트비트를 받아들이기 전에 가져온 테이블을 가진 허브 정책 저장소는 키로 인증된 요청을 차단합니다(실패 시 차단 구간 참고). |
queue_full 또는 sink_error가 증가함 | 디스크가 느리거나 가득 찼거나 디렉터리가 삭제되었습니다. 이 카운트의 레코드는 유실됩니다. |
| 라우터가 시작되지 않음 | 싱크가 설정되지 않은 환경 변수를 지정했거나, 디렉터리에 쓸 수 없습니다. |
prompt_log_capture_disabled가 1임 | 시작할 때 커서 파일을 읽지 못했습니다. 파일을 고친 뒤 재시작하십시오. |
dropped가 증가함 | on_permanent_failure: skip을 설정한 싱크가 거부된 배치를 포기했습니다. 이 레코드는 해당 대상에 전달되지 않으며, 보존 정책이 삭제할 때까지 로컬 파일에 남습니다. |
failed가 증가하고 백로그가 커짐 | 대상이 중단되었거나 요청을 거부하거나, allow_private_endpoint 없이 사설 주소로 확인됩니다. on_permanent_failure: skip을 설정하지 않으면 영구 거부는 원인을 고칠 때까지 그 싱크를 막습니다. |
pruned가 증가함 | 싱크가 읽기 전에 보존 정책이 파일을 삭제했습니다. max_files나 max_bytes를 늘리거나 장애를 더 빨리 해결하십시오. |
s3 싱크가 아무것도 보내지 않음 | prompt-log-s3 없이 빌드한 바이너리입니다. 시작할 때 경고가 남습니다. |
| 레코드가 두 번 도착함 | 전달은 최소 한 번입니다. Langfuse v4는 다시 전달된 스팬을 두 번 표시할 수 있습니다. |
| 이전 레코드가 새 싱크에 도착함 | 싱크가 backfill: true이고 유효한 커서가 없었습니다. 새 싱크이거나 커서 파일이 삭제 또는 손상된 경우입니다. |