Split View: 트레이스가 비용이 될 때 — Langfuse의 보존, 샘플링, 마스킹
트레이스가 비용이 될 때 — Langfuse의 보존, 샘플링, 마스킹
- 들어가며 — 양이 비용으로 바뀌는 순간
- 비용이 붙는 네 지점
- 샘플링 — trace 단위로 결정된다
- 샘플링에서 빼야 하는 것
- 마스킹 — 프롬프트에 개인정보가 들어갈 때
- 보존 정책 — 기본값은 삭제하지 않음
- 원본 이벤트 버킷의 수명 주기
- 큰 payload 다루기
- 비용 축을 데이터로 보기
- 마치며 — 줄이는 순서
- 직접 해보기
- 시리즈
- 참고 자료
들어가며 — 양이 비용으로 바뀌는 순간
트레이싱은 도입 초기에 거의 공짜처럼 느껴집니다. 하루 요청이 수천 건일 때는 무엇을 다 남겨도 티가 나지 않습니다. 문제는 그 시기의 습관이 굳는다는 점입니다. 요청이 하루 수백만 건이 되고 사용자가 붙여 넣은 문서가 프롬프트에 통째로 들어가기 시작하면 저장 비용과 개인정보가 동시에 옵니다. 둘은 같은 문제의 두 얼굴입니다. 필요 이상으로 많이 남기고 있다는 것이니까요.
구성과 설정 이름은 2026-08-15에 공식 문서에서 확인했습니다. Langfuse는 버전에 따라 아키텍처가 달라지므로 사용 중인 버전의 문서를 다시 확인하세요. 요금제별 세부 사항처럼 시간에 따라 바뀌는 값은 특히 그렇습니다.
비용이 붙는 네 지점
3편에서 본 저장 계층을 비용 관점으로 다시 보면 지점이 넷으로 나뉩니다.
| 지점 | 무엇이 쌓이나 | 무엇으로 줄이나 |
|---|---|---|
| ClickHouse | trace, observation, score 행 | 샘플링, 보존 정책 |
| 오브젝트 스토리지 원본 이벤트 | 수신 이벤트 전체 | 버킷 수명 주기 정책 |
| 오브젝트 스토리지 미디어 | 이미지와 첨부 | 크기 상한, 보존 정책 |
| 네트워크와 워커 | 처리량 자체 | 샘플링, 내보내기 필터 |
주목할 점은 샘플링이 위쪽 두 줄에 동시에 영향을 준다는 것입니다. 애플리케이션에서 아예 보내지 않으면 원본 이벤트도 생기지 않습니다. 반대로 보존 정책은 이미 저장된 것을 나중에 지우는 장치입니다. 순서상 샘플링이 먼저입니다.
샘플링 — trace 단위로 결정된다
샘플링 문서가 정의하는 설정은 두 가지 경로입니다. 환경 변수 LANGFUSE_SAMPLE_RATE, 그리고 생성자 인자인 파이썬의 sample_rate와 JS/TS의 sampleRate입니다. 값의 범위는 0에서 1이고 기본값은 1, 즉 전부 수집입니다.
from langfuse import Langfuse, get_client
Langfuse(sample_rate=0.5) # 50퍼센트만 수집
langfuse = get_client()
핵심은 결정 단위입니다. 문서는 SDK가 trace 수준에서 샘플링한다고 명시합니다. trace가 선택되면 그 안의 모든 observation과 score가 함께 선택되고, 선택되지 않으면 그 trace의 observation도 score도 전송되지 않습니다.
왜 중요한지는 반대 경우를 생각해 보면 압니다. observation 단위로 샘플링하면 트리에 구멍이 뚫린 트레이스가 남고, 부모는 있는데 자식이 없는 상태에서 지연을 분해하면 숫자가 틀립니다. trace 단위 결정은 남는 것은 온전하고 없는 것은 아예 없다는 성질을 보장합니다.
JS/TS SDK는 OpenTelemetry의 표준 샘플러를 씁니다.
import { TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
// 20퍼센트 수집
const sampler = new TraceIdRatioBasedSampler(0.2);
샘플링에서 빼야 하는 것
비율 샘플링에는 잘 알려진 함정이 있습니다. 드물게 일어나는 실패가 함께 사라진다는 점입니다. 하루에 열 번 나는 오류를 10퍼센트로 샘플링하면 하루 한 번만 남는데, 조사가 필요한 것은 정확히 그 열 번입니다. 그래서 현실적인 배치는 비율 하나로 끝내지 않습니다.
- 정상 경로는 낮은 비율로 남깁니다. 지연과 비용의 분포를 보는 데는 표본으로 충분합니다.
- 실패와 이상 경로는 전부 남깁니다. 코드에서 오류를 잡은 지점의 실행은 별도로 취급합니다.
- 평가 대상 데이터셋은 샘플링과 무관하게 다룹니다.
또 하나의 축은 아예 보내지 않는 것입니다. 2편에서 본 should_export_span이 그 자리입니다. 이미 OpenTelemetry를 쓰는 애플리케이션이라면 LLM 관측에 필요 없는 HTTP와 데이터베이스 스팬까지 흘러 들어오는데, is_genai_span 같은 판정 함수로 걸러 내면 처리량이 그만큼 줄어듭니다.
마스킹 — 프롬프트에 개인정보가 들어갈 때
LLM 트레이싱의 특수성이 여기서 가장 크게 드러납니다. 일반적인 애플리케이션 트레이스는 식별자와 상태 코드를 남기지만, LLM 트레이스는 사용자가 입력한 문장 전체를 남깁니다. 그리고 사용자는 무엇이든 붙여 넣습니다.
마스킹 문서가 밝히는 파이썬 쪽 방식은 두 가지이고, 차이는 적용 시점입니다. 권장되는 mask_otel_spans는 내보내기 단계, 즉 Langfuse가 어떤 OpenTelemetry 스팬을 내보낼지 결정한 뒤에 스팬 속성을 다룹니다. 예전 방식인 mask는 SDK가 속성을 만드는 시점에 동기적으로 동작합니다.
from langfuse import Langfuse
def mask_otel_spans(*, params: MaskOtelSpansParams) -> Optional[MaskOtelSpansResult]:
patches = {}
for identifier, span in params.spans.items():
patches[identifier] = OtelSpanPatch(
delete_attributes=("gen_ai.prompt.0.content",),
set_attributes={"masking.applied": True},
)
return MaskOtelSpansResult(span_patches=patches)
langfuse = Langfuse(mask_otel_spans=mask_otel_spans)
JS/TS는 LangfuseSpanProcessor에 mask 함수를 넘기는 하나의 방식입니다. 문서는 이 함수가 observation의 입력, 출력, 메타데이터에 전송 전 적용된다고 설명합니다.
import { LangfuseSpanProcessor } from "@langfuse/otel";
const spanProcessor = new LangfuseSpanProcessor({
mask: ({ data }) => {
return data.replace(/\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g, "***MASKED***");
},
});
반드시 이해해야 할 성질이 하나 있습니다. 마스킹은 애플리케이션 안에서, 데이터가 인프라를 떠나기 전에 일어납니다. 서버 쪽 기능이 아닙니다. 마스킹을 켜지 않은 서비스가 하나라도 있으면 그 서비스의 프롬프트는 원문 그대로 저장되므로, 배포 단위마다 확인해야 합니다.
정규식 마스킹은 카드 번호처럼 형태가 고정된 값에는 잘 듣지만 이름이나 주소에는 약합니다. 그래서 두 겹으로 갑니다. 형태가 있는 값은 마스킹으로 지우고, 애초에 남길 이유가 없는 필드는 2편의 capture_input과 capture_output을 꺼서 수집 자체를 막습니다.
보존 정책 — 기본값은 삭제하지 않음
데이터 보존 문서가 명시하는 기본 동작이 중요합니다. 보존 정책이 없으면 Langfuse는 이벤트 데이터를 자동으로 삭제하지 않으며, 자체 호스팅 인스턴스는 기본적으로 데이터를 무기한 보관합니다. 아무것도 설정하지 않으면 계속 쌓인다는 뜻입니다.
설정은 프로젝트 단위입니다. 프로젝트 설정 화면에서 소유자와 관리자가 조정하거나 프로젝트 API로 설정하며, 문서가 밝히는 최소 보존 기간은 3일입니다. 삭제 대상과 기준 시각도 문서에 정확히 나옵니다.
| 대상 | 기준이 되는 시각 필드 |
|---|---|
| trace | timestamp |
| observation | start_time |
| score | timestamp |
| 미디어 자산 | created_at |
삭제는 야간 작업으로 수행되며, 문서는 삭제된 자산을 복구할 수 없다고 못 박습니다. 보존 기간을 줄이기 전에 오브젝트 스토리지 내보내기를 설정해 S3, GCS, Azure로 자동 동기화해 두라는 것이 문서의 안내입니다.
자체 호스팅에는 추가 조건이 있습니다. 보존 기능을 켜려면 Langfuse IAM 역할에 모든 버킷에 대한 s3:DeleteObject 권한이 필요하고, 버전 관리가 켜진 S3 버킷이라면 삭제 마커와 이전 버전을 수동으로 지우거나 수명 주기 규칙으로 처리해야 한다고 문서가 명시합니다. 이걸 놓치면 보존 정책을 켜 놓고도 버킷 용량이 줄지 않습니다.
클라우드는 요금제에 따라 기본 접근 기간이 다릅니다. 문서를 읽은 시점 기준으로 Hobby 30일, Core 90일, Pro와 Enterprise 3년이며, 이런 값은 바뀌므로 사용 중인 요금제 문서에서 확인하세요.
원본 이벤트 버킷의 수명 주기
ClickHouse 보존 정책만 걸어 두면 절반만 한 것입니다. 3편에서 봤듯 오브젝트 스토리지에는 수신 이벤트 원본이 그대로 쌓입니다. 오브젝트 스토리지 문서는 일정 일수가 지난 객체를 자동으로 만료시키는 버킷 수명 주기 정책을 설정하라고 권고하며, 참고 값으로 Langfuse 클라우드의 30일을 제시합니다.
기간을 정할 때 고려할 것은 문서가 밝힌 원본 이벤트의 용도입니다. 재시도 작업, 재생과 재해 복구, 선택적인 레코드 조립 중 무엇을 언제까지 할 수 있어야 하는지가 곧 보존 기간입니다.
문서의 조언이 하나 더 있습니다. Langfuse는 업로드된 파일을 ClickHouse의 blob_storage_file_log 테이블로 추적하는데, 이 테이블의 수명을 버킷 정책과 맞추면 처리 최적화에 도움이 됩니다. 한쪽만 줄이면 이미 사라진 객체를 가리키는 행이 계속 남습니다.
큰 payload 다루기
멀티모달 입력과 긴 컨텍스트는 별도의 축입니다. 설정 문서에 나오는 관련 값은 다음과 같습니다.
# 미디어 관련 상한 (괄호 안이 문서 기준 기본값)
LANGFUSE_S3_MEDIA_MAX_CONTENT_LENGTH=1000000000 # 기본 1,000,000,000 바이트
LANGFUSE_S3_MEDIA_DOWNLOAD_URL_EXPIRY_SECONDS=3600 # 기본 3600 초
# 배치 내보내기 한도
BATCH_EXPORT_PAGE_SIZE=500 # 기본 500
BATCH_EXPORT_ROW_LIMIT=1500000 # 기본 1,500,000 행
# S3 동시 처리
LANGFUSE_S3_CONCURRENT_WRITES=50 # 기본 50
LANGFUSE_S3_CONCURRENT_READS=50 # 기본 50
기본 1GB라는 상한은 사실상 제한이 없는 것에 가깝습니다. 실제 서비스에서는 훨씬 낮게 잡아 두는 편이 안전합니다. 실수로 대용량 파일이 트레이스에 붙는 경로가 생겼을 때 버킷이 아니라 업로드 단계에서 막히기 때문입니다.
payload 자체를 줄이는 방법도 있습니다. 검색 결과 스무 건의 원문을 전부 남길 필요는 대개 없습니다. 문서 식별자와 점수, 상위 몇 건의 요약만 남기면 조사에 필요한 정보는 유지되면서 크기는 크게 줄어듭니다. 설정이 아니라 계측 설계의 문제이고, 2편에서 정한 경계를 다시 보는 일입니다.
비용 축을 데이터로 보기
지금까지는 트레이싱 시스템 자체의 비용이었지만, 이 시스템이 관측하는 모델 호출 비용도 같은 데이터 안에 있습니다.
1편에서 본 대로 generation에는 usage_details와 cost_details가 붙습니다. 토큰과 비용 추적 문서는 사용량이 수집되거나 추정되고 가격이 정의된 모델 정의와 매칭되면 수집 시점에 비용이 계산된다고 설명합니다. 자체 호스팅 모델이나 사내 모델은 프로젝트 설정의 모델 항목에서 정규식 패턴과 가격을 등록하며, 사용자가 정의한 모델이 Langfuse가 관리하는 모델보다 우선합니다.
모델 정의를 등록하지 않으면 그 호출들의 비용이 0으로 보인다는 뜻입니다. 비용 대시보드가 실제 청구서와 맞지 않는다면 가장 먼저 볼 곳이 여기입니다.
마치며 — 줄이는 순서
순서는 이렇습니다. 먼저 보내지 않을 것을 정합니다. 내보내기 필터와 수집 끄기입니다. 그다음 얼마나 보낼지, 즉 샘플링을 정합니다. 그다음 무엇을 가릴지, 즉 마스킹을 정합니다. 마지막으로 얼마나 오래 둘지를 정하며 보존 정책과 버킷 수명 주기를 함께 겁니다. 앞쪽일수록 비용과 위험을 동시에 줄이고, 뒤쪽일수록 이미 발생한 것을 정리하는 일입니다.
지금 할 수 있는 점검은 두 가지입니다. 보존 정책이 설정되어 있는지 프로젝트 설정에서 확인하고, 버킷 수명 주기 규칙이 걸려 있는지 스토리지 쪽에서 확인하세요. 둘 다 비어 있다면 지금 쌓이는 모든 것이 영구히 남는 중입니다.
직접 해보기
- LLM API 비용 계산기 — 모델 호출 비용을 먼저 계산해 보면 트레이싱 저장 비용과 어느 쪽이 큰지 감이 잡힙니다.
- SLO 에러 버짓 계산기 — 샘플링 비율을 정할 때 어느 정도 표본이면 목표를 판정할 수 있는지 함께 따져 보세요.
시리즈
- 이전 글: Langfuse 자체 호스팅
- 다음 글: 수집한 다음 — 대시보드, 메트릭 API, 평가
참고 자료
- Langfuse 샘플링: https://langfuse.com/docs/observability/features/sampling
- Langfuse 마스킹: https://langfuse.com/docs/observability/features/masking
- Langfuse 데이터 보존: https://langfuse.com/docs/administration/data-retention
- Langfuse 오브젝트 스토리지: https://langfuse.com/self-hosting/infrastructure/blobstorage
- Langfuse 설정 변수: https://langfuse.com/self-hosting/configuration
- Langfuse 토큰과 비용 추적: https://langfuse.com/docs/observability/features/token-and-cost-tracking
When Traces Become Cost — Retention, Sampling, and Masking in Langfuse
- Opening — The Moment Volume Turns Into Cost
- Four Places Cost Accumulates
- Sampling — The Decision Is Made Per Trace
- What to Exclude From Sampling
- Masking — When Personal Data Ends Up in the Prompt
- Retention — The Default Is Not to Delete
- Lifecycle for the Raw Event Bucket
- Handling Large Payloads
- Reading the Cost Axis From the Data
- Closing — The Order of Reduction
- Try It Yourself
- Series
- References
Opening — The Moment Volume Turns Into Cost
Tracing feels nearly free early on. At a few thousand requests a day, keeping everything costs nothing noticeable. The problem is that the habits from that period harden. When requests hit millions a day and users start pasting whole documents into prompts, storage cost and privacy arrive together. They are two faces of the same problem: you are keeping more than you need.
Component and configuration names here were verified against the official documentation on 2026-08-15. Langfuse's architecture differs by version, so re-check the docs for the version you are running — especially values like plan-specific details that change over time.
Four Places Cost Accumulates
Looking at the storage layer from the third post through a cost lens splits it into four points.
| Point | What accumulates | What reduces it |
|---|---|---|
| ClickHouse | trace, observation, and score rows | Sampling, retention policy |
| Object storage, raw events | Every ingested event | Bucket lifecycle policy |
| Object storage, media | Images and attachments | Size ceiling, retention policy |
| Network and worker | Throughput itself | Sampling, export filtering |
Note that sampling affects the top two rows simultaneously. Never send it from the application and no raw event is created either. A retention policy, by contrast, is a mechanism for deleting what is already stored. In sequence, sampling comes first.
Sampling — The Decision Is Made Per Trace
The sampling documentation defines two configuration paths: the environment variable LANGFUSE_SAMPLE_RATE, and the constructor argument sample_rate in Python or sampleRate in JS/TS. The range is 0 to 1 and the default is 1, meaning collect everything.
from langfuse import Langfuse, get_client
Langfuse(sample_rate=0.5) # collect 50 percent
langfuse = get_client()
The key is the unit of decision. The documentation states that the SDK samples on the trace level: if a trace is sampled, all observations and scores within it are sampled too, and if it is not, none of its observations or scores are sent.
Why that matters is clearest in the opposite case. Sample per observation and you are left with traces full of holes, and decomposing latency with a parent present but children missing produces wrong numbers. A trace-level decision guarantees that what survives is whole and what does not is simply absent.
The JS/TS SDK uses the standard OpenTelemetry sampler.
import { TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
// collect 20 percent
const sampler = new TraceIdRatioBasedSampler(0.2);
What to Exclude From Sampling
Ratio sampling has a well-known trap: rare failures disappear along with everything else. Sample an error that happens ten times a day at 10 percent and one instance survives — and the ten are exactly what you need to investigate. A realistic setup therefore does not stop at one ratio.
- Keep the happy path at a low ratio. A sample is enough to see the distribution of latency and cost.
- Keep failures and anomalous paths in full. Treat runs where the code caught an error separately.
- Handle evaluation datasets independently of sampling.
The other axis is not sending at all. should_export_span from the second post is the place for that. An application already running OpenTelemetry will also send HTTP and database spans that LLM observability does not need, and filtering with predicates such as is_genai_span cuts throughput by exactly that much.
Masking — When Personal Data Ends Up in the Prompt
This is where LLM tracing is most distinctive. An ordinary application trace records identifiers and status codes; an LLM trace records the entire sentence the user typed. And users paste anything.
The masking documentation describes two paths on the Python side, differing in when they apply. The recommended mask_otel_spans works at the export stage, after Langfuse has decided which OpenTelemetry spans this client will export. The older mask runs synchronously as the SDK creates attributes.
from langfuse import Langfuse
def mask_otel_spans(*, params: MaskOtelSpansParams) -> Optional[MaskOtelSpansResult]:
patches = {}
for identifier, span in params.spans.items():
patches[identifier] = OtelSpanPatch(
delete_attributes=("gen_ai.prompt.0.content",),
set_attributes={"masking.applied": True},
)
return MaskOtelSpansResult(span_patches=patches)
langfuse = Langfuse(mask_otel_spans=mask_otel_spans)
JS/TS has a single path: a mask function passed to LangfuseSpanProcessor. The documentation says it is applied to observation input, output, and metadata before sending.
import { LangfuseSpanProcessor } from "@langfuse/otel";
const spanProcessor = new LangfuseSpanProcessor({
mask: ({ data }) => {
return data.replace(/\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g, "***MASKED***");
},
});
There is one property you have to internalize. Masking happens inside the application, before the data leaves your infrastructure. It is not a server-side feature. If even one service has masking turned off, that service's prompts are stored verbatim, so this has to be verified per deployable unit.
Regex masking works well on fixed-shape values like card numbers and poorly on names and addresses. So you go two layers deep: erase shaped values with masking, and for fields there was never a reason to keep, turn off collection entirely with capture_input and capture_output from the second post.
Retention — The Default Is Not to Delete
The default behavior the data retention documentation states matters. Without a retention policy, Langfuse does not automatically delete event data, and self-hosted instances store data indefinitely by default. Configure nothing and it accumulates forever.
Configuration is per project. Owners and administrators adjust it in project settings, or you set it through the projects API, and the documentation states a minimum retention window of three days. The deletion targets and the timestamp fields they key off are documented precisely as well.
| Target | Timestamp field used |
|---|---|
| trace | timestamp |
| observation | start_time |
| score | timestamp |
| media assets | created_at |
Deletion runs as a nightly process, and the documentation is blunt that deleted assets cannot be recovered. Before shortening a retention window, the documentation's guidance is to set up Blob Storage Export so data syncs automatically to S3, GCS, or Azure.
Self-hosting carries an extra condition. Enabling retention requires granting the Langfuse IAM role s3:DeleteObject across all buckets, and on versioned S3 buckets the documentation states that delete markers and non-current versions must be removed manually or through lifecycle rules. Miss that and bucket usage will not shrink even with a retention policy turned on.
On Cloud, the default access window varies by plan. As of the date these docs were read: Hobby 30 days, Core 90 days, Pro and Enterprise 3 years. Values like these change, so check the documentation for the plan you are on.
Lifecycle for the Raw Event Bucket
Setting a ClickHouse retention policy alone is only half the job. As the third post covered, the raw ingested events pile up in object storage. The object storage documentation recommends configuring a bucket lifecycle expiration policy that automatically deletes objects after a defined number of days, offering 30 days on Langfuse Cloud as a reference point.
What to weigh when choosing that window is the documented purpose of raw events: retry jobs, replay and disaster recovery, and optional record assembly. How long you need to be able to do those is your retention window.
The documentation adds one more piece of advice. Langfuse tracks uploaded files in the ClickHouse blob_storage_file_log table, and matching that table's TTL to the bucket policy helps optimize processing. Shrink only one side and rows pointing at objects that no longer exist keep piling up.
Handling Large Payloads
Multi-modal inputs and long contexts are their own axis. The relevant values from the configuration documentation are as follows.
# Media limits (documented defaults in the comments)
LANGFUSE_S3_MEDIA_MAX_CONTENT_LENGTH=1000000000 # default 1,000,000,000 bytes
LANGFUSE_S3_MEDIA_DOWNLOAD_URL_EXPIRY_SECONDS=3600 # default 3600 seconds
# Batch export limits
BATCH_EXPORT_PAGE_SIZE=500 # default 500
BATCH_EXPORT_ROW_LIMIT=1500000 # default 1,500,000 rows
# S3 concurrency
LANGFUSE_S3_CONCURRENT_WRITES=50 # default 50
LANGFUSE_S3_CONCURRENT_READS=50 # default 50
A default ceiling of 1 GB is effectively no ceiling at all. In a real service it is safer to set this far lower, so that when a path accidentally attaches a large file to a trace it is blocked at upload rather than in the bucket.
There is also the option of shrinking the payload itself. You rarely need the full text of twenty retrieved documents. Keeping document identifiers, scores, and a summary of the top few preserves what investigation needs while cutting size dramatically. That is not a setting but an instrumentation design question — it means revisiting the boundary you drew in the second post.
Reading the Cost Axis From the Data
So far this has been the cost of the tracing system itself, but the cost of the model calls it observes lives in the same data.
As the first post covered, generations carry usage_details and cost_details. The token and cost tracking documentation explains that cost is calculated at ingestion when usage is ingested or inferred and matches a model definition that includes prices. For self-hosted or in-house models you register a regex pattern and prices under the models entry in project settings, and user-defined models take priority over the models Langfuse maintains.
Which means: register no model definition and those calls show a cost of zero. If your cost dashboard does not line up with the actual bill, this is the first place to look.
Closing — The Order of Reduction
The order goes like this. First decide what not to send — export filters and turning off capture. Then decide how much to send, which is sampling. Then decide what to hide, which is masking. Finally decide how long to keep it, applying a retention policy and a bucket lifecycle together. The earlier steps reduce cost and risk at the same time; the later ones clean up what has already happened.
There are two checks you can run today. Confirm in project settings whether a retention policy exists, and confirm on the storage side whether a bucket lifecycle rule is in place. If both are empty, everything accumulating right now is being kept forever.
Try It Yourself
- LLM API Cost Calculator — calculating model call cost first tells you which side is bigger, that or tracing storage.
- SLO & Error Budget Calculator — when choosing a sampling ratio, work out how large a sample you need to judge the target at all.
Series
- Previous: Self-Hosting Langfuse
- Next: After You Have Collected It — Dashboards, the Metrics API, and Evaluation
References
- Langfuse sampling: https://langfuse.com/docs/observability/features/sampling
- Langfuse masking: https://langfuse.com/docs/observability/features/masking
- Langfuse data retention: https://langfuse.com/docs/administration/data-retention
- Langfuse blob storage: https://langfuse.com/self-hosting/infrastructure/blobstorage
- Langfuse configuration variables: https://langfuse.com/self-hosting/configuration
- Langfuse token and cost tracking: https://langfuse.com/docs/observability/features/token-and-cost-tracking