
이 글은 Langfuse 실습 시리즈의 10편입니다. 에이전트를 관측하는 시스템 자체를 운영하기 위해 마스킹·수집 실패·백업 복원을 확인합니다.
이 글에서 다룰 주제
주요 단어 · Masking · Exporter · Ingestion · Retention · RPO · RTO
에이전트를 관측하는 도구도 장애가 날 수 있다. 입력과 출력에 민감정보가 섞일 수도 있고, 대시보드는 정상처럼 보이지만 새 데이터가 들어오지 않을 수도 있다. 마지막 편은 무엇을 저장하고, 수집 실패를 어떻게 알아차리며, 저장한 데이터를 실제로 복구할 수 있는가를 다룬다.
실습은 로컬 Langfuse v4.50.0 OSS와 Python SDK 4.16.0에서 진행했다. 실제 개인정보나 운영 비밀을 넣지 않고, 테스트용 이메일과 토큰 표식을 사용했다. 마스킹·수집 확인에는 모델 호출이 필요하지 않아 Ollama를 추가 호출하지 않는다. 아래 공식 기능 범위는 2026년 10월 5일 확인했다.

그림은 애플리케이션에서 데이터가 나오는 경로와 저장소를 복원하는 경로를 분리해서 보여 준다. 마스킹은 기록을 내보내기 전에 적용하고, 백업은 저장된 데이터를 복원 가능한 형태로 보존한다. 한쪽으로 다른 쪽을 대신할 수 없다.
이번 구성의 데이터 경로는 다음과 같다.
애플리케이션
→ SDK 마스킹·배치 전송
→ Langfuse 수집 경로
→ 저장소와 Worker 처리
→ UI·Metrics·평가·알림
Exporter는 관측 데이터를 외부 시스템으로 보내는 구성요소다. Ingestion은 전송된 데이터를 받아 저장·처리하는 수집 과정이다. 애플리케이션이 응답을 반환한 시점과 이 과정이 끝난 시점은 같지 않을 수 있다.
따라서 “사용자 요청 정상”, “SDK 전송 정상”, “Worker 처리 정상”, “최신 데이터 조회 가능”을 나누어 확인한다. 웹 화면에 접속된다는 사실만으로 이 경로 전체가 정상이라고 판단하지 않는다.
이번에는 Python SDK의 mask_otel_spans를 사용했다. 내보낼 OpenTelemetry span의 문자열 속성에서 테스트용 이메일과 토큰 표식을 교체한다. 새 Python 설정에는 이 훅이 권장되며, 기존 mask와 적용 범위·시점이 다르다. 공식 SDK 마스킹 문서
핵심 코드는 다음과 같다. 실제 실습의 operations.py에서 사용한 함수다.
from langfuse.types import MaskOtelSpansResult, OtelSpanPatch
def redact(*, params):
patches = {}
for identifier, span in params.spans.items():
replacements = {}
for key, value in span.attributes.items():
if isinstance(value, str):
masked = value.replace(
"learner@example.test", "[REDACTED_EMAIL]"
).replace(
"LAB_TOKEN_DO_NOT_EXPORT", "[REDACTED_TOKEN]"
)
if value != masked:
replacements[key] = masked
if replacements:
replacements["masking.applied"] = True
patches[identifier] = OtelSpanPatch(
set_attributes=replacements
)
return MaskOtelSpansResult(span_patches=patches)
이 함수를 Langfuse(..., mask_otel_spans=redact)에 전달했다. 입력, 출력, 메타데이터에 테스트 표식을 넣어 한 건을 기록한 뒤, 저장된 관측을 API로 다시 읽었다.
| 확인 항목 | 실제 결과 |
|---|---|
| 애플리케이션 안의 원본 입력 | 그대로 유지 |
| 저장된 이메일 | [REDACTED_EMAIL] |
| 저장된 토큰 표식 | [REDACTED_TOKEN] |
| 저장 관측의 원본 이메일·토큰 검색 | 발견되지 않음 |
| 마스킹용 모델 호출 | 0회 |
Trace 이름은 aiops-export-masking, 환경은 aiops-lab이다. 상세 화면에서 Input, Output, Metadata를 각각 읽으면 한 필드만 가리고 다른 필드에 원문을 남기는 실수를 점검할 수 있다.

Tracing에서 aiops-export-masking을 열면 Input과 Output은 [REDACTED_EMAIL]·[REDACTED_TOKEN], Metadata의 contact도 대체된 값으로 보인다. 이 화면은 UI에서만 가린 것이 아니라 SDK가 전송 전에 바꾼 결과다.
이 실습은 알려진 두 문자열을 바꾸는 최소 예제다. 모든 개인정보나 비밀을 찾아내는 탐지기가 아니다. 실무에서는 처음부터 기록할 필드를 제한하고, 중첩 데이터·도구 응답·에러 메시지·별도 로그 저장 경로까지 정책을 정해야 한다.
또한 SDK 마스킹은 Langfuse로 나가는 관측 데이터를 바꾼다. 애플리케이션이 LLM에 보낸 입력, 애플리케이션 메모리의 원본, 다른 exporter가 보낸 데이터까지 자동으로 바꾸지는 않는다. 모델에 민감정보를 보내지 않아야 한다면 모델 요청 전에 별도 처리가 필요하다.
마스킹 함수 자체도 운영 코드다. 이번 SDK 문서에 따르면 훅이 예외를 던지거나 잘못된 결과를 반환하면 전체 내보내기 배치가 버려질 수 있다. 외부 API를 호출하는 복잡한 마스킹 함수를 넣기 전에 실패 시 동작과 처리 시간을 검증해야 한다.
서버 측 마스킹은 여러 클라이언트에 공통 정책을 적용하기 편하다. 그러나 현재 셀프호스팅에서는 Enterprise 기능이며, Worker가 비동기로 처리하기 전에 원시 이벤트가 Blob Storage에 먼저 들어갈 수 있다. “민감정보가 애플리케이션 밖으로 절대 나가면 안 된다”는 요구에는 클라이언트에서 내보내기 전에 처리하는 경계가 중요하다. 공식 서버 마스킹 문서
이번 실습은 OSS의 SDK 마스킹만 사용했다. 서버 측 마스킹 라이선스나 외부 처리 서비스를 추가하지 않았다. 계정에서 보이는 화면 이름만 보고 기능이 활성화돼 있다고 가정하지 않고 실제 적용 지점과 저장 결과를 확인했다.
수집 실패를 보기 위해 실제 Langfuse 서버를 끄는 대신, 별도의 Python 프로세스 한 개만 연결할 수 없는 loopback 주소로 설정하는 실습을 실행했다. 비어 있는 포트를 무작정 고르지 않고, 로컬 소켓을 예약하되 연결을 받지 않도록 해서 다른 서비스에 요청이 갈 가능성을 줄였다.
실습의 비교 대상은 두 개다.
여기서 업무 결과는 합성 요청의 접수 상태이며 LLM 답변이 아니다. 이 방법은 SDK의 연결 실패를 관찰하기 위한 실습이다. Redis나 ClickHouse 장애, Worker 중단, 서버 내부 큐 복구를 시험하는 것과 범위가 다르다.
docs/langfuse-study-2026-10-04/lab/.venv/bin/python \
docs/langfuse-aiops-2026-10-05/lab/ingestion_probe.py
이미 실행한 뒤에는 다음 명령으로 저장된 Trace ID만 다시 확인한다. 새로운 관측이나 모델 호출을 만들지 않는다.
docs/langfuse-study-2026-10-04/lab/.venv/bin/python \
docs/langfuse-aiops-2026-10-05/lab/ingestion_probe.py --verify-only
실행 기록은 evidence/ingestion-probe.json에 남겼다. 실제 결과는 다음과 같다.
| 항목 | 연결 불가 SDK | 정상 주소의 새 SDK |
|---|---|---|
| 계산한 업무 결과 | accepted, read_only=true | 동일 |
| Exporter 오류 로그 | ERROR 발생 | 없음 |
명시적 flush() 소요 | 약 2.002초 | 약 0.047초 |
flush() 반환값 | None | None |
| 실제 서버에서 해당 Trace 조회 | 확인 시점에 0건 | 새 Trace 1건 |
실습용 SDK의 timeout은 2초로 설정했다. 오류 로그는 Failed to export spans batch due to timeout, max retries or shutdown.이었다. 원본 Langfuse 서버는 이 과정에서 계속 동작했다. 관측 ID·출력·환경과 오류 기록을 포함한 11개 검사가 통과했다.
정상 Trace 이름은 aiops-ingestion-normal, 환경은 aiops-ingestion-probe다. 이 실습은 정상 주소의 새 요청이 다시 기록됨을 확인한 것이다. 먼저 실패한 span을 자동 복구하거나 재전송했다고 표현하지 않는다. 실패한 Trace가 확인 시점에 없었다는 결과와 모든 장애 상황의 영구 손실 여부를 일반화하는 주장도 구분한다.
flush()는 짧게 실행되는 스크립트에서 데이터를 내보낼 기회를 주는 데 필요하다. 하지만 이 SDK의 반환값은 영구 저장 성공을 증명하는 영수증이 아니다. 성공 여부가 중요하면 실제 조회 가능 여부와 exporter 오류를 함께 확인해야 한다.
운영에서는 애플리케이션 성공률 외에 데이터의 최신성을 본다. 일정 간격으로 알려진 소량의 관측을 보내고, 일정 시간 안에 조회되는지 확인하는 방식이다. 요청 발생 시각과 조회 가능 시각의 차이가 수집 경로의 지연을 드러낸다. 이 실습에서 상시 모니터링 작업을 설치한 것은 아니며, 운영 도입 시 추가할 항목이다.
함께 볼 값은 다음과 같다.
| 영역 | 확인할 값 | 의미 |
|---|---|---|
| 애플리케이션 SDK | 내보내기 오류·재시도·드롭 | 서버 도착 전 손실 가능성 |
| Langfuse Worker | 대기 큐 깊이·처리율 | 수집량에 비해 처리가 밀리는지 |
| 저장소 | 디스크 사용량·읽기/쓰기 지연 | 저장 용량과 병목 |
| 조회 경로 | 최신 관측의 지연·누락 | 사용자가 볼 수 있는 데이터의 신선도 |
| 평가·알림 | 평가 대기·전달 실패 | 관측은 됐지만 후속 조치가 멈췄는지 |
현재 공식 스케일링 문서는 Worker의 StatsD 지표 langfuse.queue.ingestion.depth와 type:waiting 태그를 설명한다. 이 글에서는 해당 지표를 Prometheus로 수집하는 별도 스택까지 설치하지 않았다. 도입 환경의 메트릭 수집 경로에 맞춰 연결할 대상이다. 공식 스케일링 문서
데이터가 끊겼을 때 비용 0, 오류 0이라는 숫자가 나오면 정상으로 오해하기 쉽다. No Data를 실제 0과 구분하고, “새 요청이 없었음”과 “수집을 못 했음”을 비교할 신호가 필요하다.
앞선 Alerts 실습의 수신기는 Langfuse와 같은 Docker 네트워크의 aiops-webhook이다. URL은 https://aiops-webhook/alerts이며 호스트에 수신 포트를 공개하지 않았다. Langfuse에서 내부 수신기로 나가는 경로만 사용한다.
실습 설정은 내부 호스트 한 개를 정확히 허용하고, 해당 실습 인증서를 신뢰하도록 지정했다.
LANGFUSE_WEBHOOK_WHITELISTED_HOST: aiops-webhook
NODE_EXTRA_CA_CERTS: /study/aiops-webhook-ca.crt
인증서의 SAN은 aiops-webhook이며 유효기간은 2026년 10월 5일~11월 4일의 30일이다. 장기 운영용 인증서 자동 갱신을 구성한 것은 아니다. TLS 검증 전체를 끄는 방식으로 연결하지 않았다.
수신기는 본문의 HMAC 서명과 타임스탬프를 확인하고, 같은 이벤트 ID는 다시 저장하지 않는다. 인증 헤더나 서명 비밀값을 로그에 출력하지 않는다. 서버가 알림을 보냈다는 기록과 수신기가 검증해서 받아들였다는 기록을 구분해 확인할 수 있게 한 것이다.
현재 실습 폴더의 .private 디렉터리에는 수신기 TLS 키·인증서와 Webhook 서명 비밀값이 있다. 기존 환경을 계속 사용할 때 이 폴더를 삭제하면 안 된다. Compose가 인증서를 Web·Worker에 마운트하므로, 1편의 init_env.py로 .env만 만들어서는 현재 확장 구성을 새 컴퓨터에서 재현할 수 없다.
새 환경으로 복사할 때는 그 환경에 맞는 TLS 키·인증서(SAN aiops-webhook)와 신뢰 인증서를 다시 생성하고, Webhook 설정의 서명 비밀값을 수신기와 일치시켜야 한다. 비공개 파일을 공개 저장소나 블로그에 올려 해결하지 않는다. 기존 백업을 복원하는 경우에는 대응하는 비공개 설정을 안전하게 보존해야 한다. 파일 위치와 재실행 순서는 실습 폴더의 README.md에 정리했다.
다만 이 설정은 로컬 학습용이다. 운영 연결에서는 인증서 발급·갱신, 비밀값 회전, 수신기 가용성, 이벤트 중복 처리와 재전송 정책을 실제 운영 요구에 맞춰 정해야 한다.
Retention은 데이터를 얼마나 오래 보관할지 정하는 정책이다. 현재 Langfuse의 프로젝트 Data Retention 기능은 셀프호스팅 Enterprise 기능이며, 이 OSS 실습에는 자동 만료 정책을 적용하지 않았다. 셀프호스팅에서는 정책이 없으면 이벤트가 기본적으로 계속 보관된다. 공식 Data Retention 문서
보관 정책을 만들 때 Trace와 Dataset도 구분해야 한다. Dataset에 저장한 입력·기대 출력·메타데이터는 원본 Trace와 별도의 자료다. 프로젝트 이벤트 보관 기간이 끝나 원본 Trace가 삭제돼도 Dataset item은 남을 수 있다. 반대로 Dataset에 넣었다는 이유로 원본 Trace의 상세 관측까지 영구 보존되는 것은 아니다.
운영에서 보관 범위를 정할 때는 실시간 조사용 Trace, 회귀검사용 Dataset, 감사 기록, 백업 복사본을 각각 다룬다. 화면에서 삭제했다고 백업이나 버전이 있는 오브젝트 저장소의 이전 복사본까지 사라졌다고 가정하면 안 된다.
또한 조직 수준의 접근 제어와 프로젝트별 세부 권한은 같은 기능이 아니다. 현재 공식 문서상 프로젝트 수준 RBAC, 보호된 Prompt Label, 보관 정책, Audit Logs, 서버 마스킹은 Enterprise 라이선스 대상이다. 이번 실습은 이 기능을 사용했다고 주장하지 않는다. 공식 셀프호스팅 라이선스 범위
백업의 목표는 압축파일을 만드는 것에서 끝나지 않는다. 다른 환경에서 필요한 데이터를 읽을 수 있어야 한다. RPO는 감당할 수 있는 데이터 손실 구간, RTO는 서비스 복구에 허용하는 시간이다. 두 값을 정해야 백업 주기와 복원 절차를 설계할 수 있다.
Langfuse의 관측 데이터는 ClickHouse, 사용자·프로젝트·설정 등은 PostgreSQL, Blob 데이터는 오브젝트 저장소와 연결된다. 한 데이터베이스만 백업하고 전체 환경을 복구할 수 있다고 가정하면 안 된다. 공식 백업 가이드
이번 로컬 복원 실습은 작은 환경에 맞춰 일관된 시점의 볼륨 복사 방식으로 실행했다.
복원본은 별도 Docker 프로젝트와 외부 통신을 제한한 내부 네트워크를 사용한다. 복원본 Worker와 Webhook 수신기를 실행하지 않아 보관된 알림이나 평가가 다시 작동하는 일을 막는다. 원본 볼륨을 지우거나 덮어쓰는 절차가 아니다.
실제 결과는 다음과 같다. 정지된 상태의 스냅샷 시각은 2026년 10월 5일 14:15:40 KST였고, 압축은 14:16:09 KST에 끝났다.
| 확인 항목 | 실제 결과 |
|---|---|
| 원본 중지부터 health 회복까지 | 82.677초 |
| 백업한 볼륨 | 5개 |
| 압축파일 합계 | 385,577,254 bytes, 약 367.7 MiB |
| 복원본의 확인 대상 | Prompt v1, Trace 3개, 관측 18개, 작업 Score 3개 |
| 원본·복원본 비교 | 선택한 내용의 해시 일치 |
| 원본 재시작 전후 | 선택한 내용의 해시 일치 |
| 검증 종료 후 | 복원본 5개 서비스 중지, 원본 7개 서비스 실행 |
| 백업·복원 확인 | 9개 검사 통과 |
처음에는 복원본의 호스트 포트로 health를 조회하려 했지만 Docker 내부 네트워크 설정 때문에 해당 경로로 접근할 수 없어 대기 검사가 실패했다. 네트워크 격리를 해제하지 않고, 복원본 컨테이너 안에서 같은 공개 API를 호출해 필요한 데이터를 비교했다. 최종 상태는 verified-via-internal-api로 남겼으며, 최초 접근 실패 기록도 별도로 보관했다.
따라서 확인한 것은 격리 복원본에서 선택한 기존 데이터를 API로 읽을 수 있다는 것이다. 복원본 UI 로그인, 새 모델 요청, Worker 평가 처리, 모든 레코드·미디어의 전수 복원을 확인한 것은 아니다. 82.677초도 이번 원본 서비스의 중지·재시작 구간 측정이며, 운영 재해 상황의 RTO 보장이 아니다.
측정값과 검사 결과는 evidence/backup-restore.json에 남겼다. 이번 방식은 작은 로컬 환경의 정지 스냅샷 실습이다. 무중단 운영이나 특정 시점 복원(PITR)이 필요하면 서비스별 백업 체계와 복구 절차를 따로 설계해야 한다.
백업에는 프로젝트 키와 암호화 설정처럼 민감한 정보가 포함될 수 있다. 실습 파일은 전용 비공개 폴더와 제한된 파일 권한으로 저장한다. 권한 제한과 파일 암호화는 서로 다른 보호 수단이다. 외부 저장소로 옮기는 운영 백업에는 암호화와 복호화 키 관리, 정기 복원 점검이 추가로 필요하다.
이번 과정에서 Langfuse로 얻은 것은 Agent·Tool 실행 기록, 프롬프트 버전 연결, 평가 결과, 비용·지연 집계, 알림, 실패를 회귀검사로 되돌리는 흐름이다.
반면 권한 없는 도구 호출 차단, 요청당 최대 비용, 토큰·반복·시간 상한, 쓰기 작업 승인, 즉시 중단은 에이전트 실행 코드나 게이트웨이에서 적용해야 한다. 비동기 평가·알림이 나중에 위험을 발견하는 것과, 실행 전에 위험한 행동을 막는 것은 시점이 다르다.
이번 unknown-injection 사례는 이 차이를 보여 줬다. 모델 판단은 악의적인 합성 로그에 영향을 받았지만, 프로그램에 쓰기 도구를 제공하지 않아 삭제나 재시작을 실행할 수는 없었다. 올바른 판단과 제한된 권한을 각각 검증해야 한다.
운영 도입의 다음 실험은 더 많은 모델을 연결하는 것보다, 대표 장애 사례를 늘리고 실패를 다시 재현하는 것에서 시작할 수 있다. 사례별 기대 행동, 사람에게 넘길 조건, 비용·지연 허용 범위, 관측 데이터의 수집·복원 가능성을 함께 관리하면 에이전트 변경의 영향을 설명할 수 있다.
실습·공식 문서 확인: 2026-10-05. Langfuse OSS v4.50.0, Python SDK 4.16.0. 합성 표식 마스킹, 별도 SDK의 연결 실패와 새 요청 수집, 격리된 복원본의 기존 데이터 조회를 실제 확인했습니다. 복원본 UI 로그인·Worker 신규 처리·전체 데이터 전수 복원은 검증 범위에 포함하지 않았습니다.