Langfuse v4, día 2 (4 de 10): migrar de la 3 a la 4 sin ventana, y dónde está el punto de no retorno
Índice
Cuarto artículo de una serie de diez sobre operar Langfuse v4 en producción. Los tres anteriores miraban el lado del cliente: qué entra en una traza, lo que cuesta un turno de agente y dos agentes conocidos instrumentados. Este es el primero del lado del servidor. Verificado contra el código de Langfuse 4.35.0, commit
2ee1908del 14 de septiembre de 2026.
TL;DR
El valor por defecto del modo de escritura es el destino, no el origen. LANGFUSE_MIGRATION_V4_WRITE_MODE sale de fábrica en events_only (packages/shared/src/env.ts:337). Eso está bien para una instalación nueva y está mal para una migración: en ese modo las tablas traces y observations dejan de escribirse.
El worker se niega a arrancar ante tres combinaciones. legacy con escritura OTel directa, events_only sin la opción de vista previa, y events_only con el comportamiento OTel en dual. El comentario del código las llama por su nombre: combinations that would silently lose data (worker/src/env.ts:722).
En events_only veinticuatro rutas REST devuelven 404. Trazas, observaciones, sesiones, scores v1 y v2, métricas, spans, generations y las de dataset runs. No es un fallo, es un cortocircuito deliberado para no servir datos viejos de tablas que ya nadie escribe.
El histórico lo rellena una cadena de cinco migraciones de fondo, no las migraciones de ClickHouse. Ninguna de las diez migraciones nuevas de ClickHouse copia un solo dato antiguo.
Una migración de fondo fallida no bloquea a la siguiente. El gestor no tiene modelo de dependencias y las salta. La cadena se protege con guardas de predecesor que los dos primeros pasos no declaran, así que un fallo del paso 1 no impide que arranque el paso 2.
Los diez minutos de retraso de la API vieja tienen una explicación exacta. Particiones de tres minutos en la tabla de paso, bloqueo cuatro minutos después de crearse, y un cron por minuto que solo toca particiones de más de diez minutos, con concurrencia global de uno.
Esa tabla de paso tiene un TTL de cuarenta y ocho horas. Si el propagador se para un fin de semana largo, esas particiones desaparecen y el hueco en events_full es permanente. No hay código que lo detecte ni que lo repare.
El punto de no retorno está en Postgres, no en ClickHouse. Una migración de Prisma hace DROP TABLE sobre traces, observations y scores, y Prisma Migrate no tiene reversa.
Estás aquí: OBSERVE, día 2
Los tres primeros artículos median lo que produce un agente. Este empieza la parte de operar el backend que lo recibe, y va antes que el de capacidad porque no se puede dimensionar lo que todavía no se ha migrado.
La analogía: cambiar el ancho de vía sin cortar el servicio
Cambiar el ancho de vía de una línea en explotación no se hace de golpe. Se tiende el tercer carril, se hace circular material de los dos anchos durante un tiempo, y solo cuando todos los trenes son del ancho nuevo se levanta el carril viejo.
La migración de Langfuse tiene esa forma exacta. El modo dual es el tercer carril: se escribe en las tablas viejas y en las nuevas a la vez. Y como en la vía, el peligro no está en el día del cambio, está en los dos extremos. Empezar tendiendo el carril nuevo y levantando el viejo a la vez deja trenes sin poder circular. Y una vez levantado el carril viejo, volver atrás no es cuestión de voluntad, es cuestión de que el material ya no está.
La escalera de tres peldaños
Todo gira alrededor de una variable con tres valores, definida por triplicado en los tres paquetes que tienen que estar de acuerdo, con el mismo enum y el mismo valor por defecto (packages/shared/src/env.ts:337, worker/src/env.ts:586, web/src/env.mjs:610):
LANGFUSE_MIGRATION_V4_WRITE_MODE: z
.enum(["legacy", "dual", "events_only"])
.default("events_only"),
Los dos ayudantes que la traducen dicen lo que hace cada peldaño mejor que cualquier explicación (worker/src/env.ts:705):
export const v4WritesToEventsTable = (envValue: ParsedEnv): boolean =>
envValue.LANGFUSE_MIGRATION_V4_WRITE_MODE !== "legacy";
export const v4WritesToLegacyTables = (envValue: ParsedEnv): boolean =>
envValue.LANGFUSE_MIGRATION_V4_WRITE_MODE !== "events_only";
Qué se escribe en cada modo:
| Tabla | legacy | dual | events_only |
|---|---|---|---|
traces, observations | sí | sí | no |
scores | sí | sí | sí |
dataset_run_items_rmt | sí | sí | no |
observations_batch_staging | no | sí | no |
events_full | no | sí | sí |
events_core | no | sí (por la vista materializada) | sí |
El peldaño intermedio es el único donde conviven las dos cosas, y por eso es el único sitio desde el que se puede volver atrás sin perder nada. El propio código lo propone como remedio cuando algo falla en events_only (web/src/pages/api/public/ingestion.ts:296):
As a temporary migration bridge, set
LANGFUSE_MIGRATION_V4_WRITE_MODE=dualon both the web and worker services and redeploy.
El both de esa frase importa. Los tres paquetes leen la variable y el comentario del código insiste tres veces en mantenerlos sincronizados. Desplegar el worker con un valor y la web con otro es la primera manera de romper esto.
Las tres combinaciones que no arrancan
El worker valida la configuración al arrancar y lanza excepción en tres casos, bajo un comentario que no deja dudas (worker/src/env.ts:718):
// Hard errors: combinations that would silently lose data.
if (mode === "legacy" && otel === "direct") {
throw new Error(
"Invalid V4 config: LANGFUSE_MIGRATION_V4_NATIVE_OTEL_BEHAVIOUR=direct " +
"requires LANGFUSE_MIGRATION_V4_WRITE_MODE in {dual, events_only}. " +
"Direct OTel writes target events_full, which is not read in legacy mode.",
);
}
El segundo es el que muerde en una migración real: events_only exige además LANGFUSE_MIGRATION_V4_ALLOW_PREVIEW_OPT_IN=true, porque si no las lecturas de la web apuntarían a las tablas legacy que ese modo ya no escribe. El tercero prohíbe events_only con el comportamiento OTel en dual.
Que el arranque falle es la buena noticia. Un fallo de arranque se ve en treinta segundos. Las combinaciones que no valida, en cambio, tardan días en dar la cara.
Qué se rompe en cada modo
Esta es la tabla que hay que tener delante antes de mover la variable, porque los dos extremos rompen cosas distintas.
En legacy, las tablas de eventos no se escriben, así que lo que depende de ellas desaparece. Las rutas de monitores devuelven 404 por un middleware específico (web/src/server/api/trpc.ts:413). Las métricas v2 no están disponibles. Los widgets de cuadro de mando se quedan en la versión 1 (DashboardService.ts:39). La vista previa de la v4 está forzada a apagado y no se puede encender.
En events_only, las tablas legacy no se escriben, así que veinticuatro rutas REST se cortocircuitan con 404 (web/src/features/public-api/server/createAuthedProjectAPIRoute.ts:319):
// Short-circuit routes that read from legacy traces/observations tables
// when the deployment is in events_only mode — those tables are no longer
// populated, so the response would be stale or empty.
La lista cubre traces, traces/{id}, observations, observations/{id}, sessions, sessions/{id}, scores y scores/{id} en v1 y en v2, metrics, metrics/daily, spans, generations, events, dataset-run-items y las dos de datasets/{name}/runs. Si tienes integraciones propias contra esas rutas, se caen el día del cambio, y devolver 404 en vez de datos rancios es la decisión correcta aunque duela.
Un detalle que se suele contar mal: /api/public/ingestion no se apaga nunca. Lo dice el propio aviso de deprecación (deprecations.ts:104):
This route is never removed: scores (and sdk-log) stay accepted.
En events_only esa ruta sigue viva y sigue aceptando dos tipos de evento (web/src/pages/api/public/ingestion.ts:274):
const EVENTS_ONLY_ALLOWED_TYPES = new Set<string>([
eventTypes.SCORE_CREATE,
eventTypes.SDK_LOG,
]);
El rechazo del resto es por evento, no por lote: el lote devuelve un 207 con errores 400 dentro. Un cliente viejo que mande trazas y scores mezclados verá pasar los scores y fallar el resto, que es justo el tipo de fallo parcial que tarda en detectarse.
Los tres pasos, en orden
Con lo anterior, el orden de una migración sin ventana es este:
- Terminar las migraciones de fondo de la v3 antes de subir. Esto es previo a todo lo demás y no admite atajos. La v4 borra las filas de siete migraciones antiguas, y el comentario explica por qué (
prisma/migrations/20260723124010_drop_dataset_run_items_table/migration.sql:7): sus scripts se borran con ellas, así que una fila sin terminar dejaría al gestor sin script que resolver. Si alguna quedó a medias, esos datos se pierden y la fila desaparece sin ruido. - Subir a v4 con
LANGFUSE_MIGRATION_V4_WRITE_MODE=legacy, aplicar las migraciones de ClickHouse y las de Prisma. Aquí la instalación se comporta como la v3 y se comprueba que todo sigue en pie. - Pasar a
dual. A partir de ahí se escriben las dos familias de tablas y arranca el propagador. Se deja correr el tiempo que haga falta y se lanza el relleno del histórico. - Cuando el histórico esté completo y verificado, pasar a
events_onlycon la opción de vista previa activada, y solo entonces levantar el carril viejo.
Entre el 3 y el 4 está todo el trabajo, y puede durar semanas. No hay prisa: dual es un estado estable.
Las migraciones de ClickHouse no rellenan nada
Hay 96 ficheros en packages/shared/clickhouse/migrations/canonical/, 48 de subida y 48 de bajada, que hay que materializar antes de ejecutar (pnpm ch:migrations:materialize) y limpiar después, incluso si la migración falló. El ejecutor es golang-migrate.
Las diez de la v4 son 0039 a 0048. Las tres primeras crean events_full, events_core y la vista materializada que alimenta la segunda desde la primera. Las tres últimas de borrado tiran event_log, project_environments y dataset_run_items. Las de en medio añaden columnas e índices.
Lo importante para planificar es lo que no hacen. Ninguna copia un dato antiguo. La vista materializada solo ve las filas que se inserten en events_full a partir de su creación. Los índices de las migraciones 0043 y 0047 se añaden sin materializar las partes históricas, y la 0047 lo dice por escrito:
Skip indexes are added without materializing historical parts to keep this migration metadata-only.
Traducido a operación: durante y después de la migración, las búsquedas sobre datos anteriores hacen escaneo completo. No es un fallo, es el precio de que la migración no bloquee.
Y una trampa de clúster más: las migraciones 0042, 0043, 0047 y 0048 llevan alter_sync = 2, o sea que cada ALTER espera a todas las réplicas. Una réplica lenta o caída cuelga la migración entera. En una instalación de una sola réplica esto no se nota; en tres, sí.
La cadena de cinco pasos que sí rellena el histórico
El relleno lo hacen cinco migraciones de fondo que corren en el worker, registradas en Postgres con nombres que llevan el orden dentro:
20260701_v4_step_1_create_root_spans_from_traces: crea spans raíz virtuales enevents_fulla partir de las trazas existentes, con el identificadorconcat('t-', t.id). Salta las trazas referenciadas por un dataset run item, que son del paso 4. Trocea por partición mensual.20260701_v4_step_2_rewrite_observations_to_pid_tid_sorting: copiaobservationsa una tabla de trabajo con otro orden de claves. Trocea por partición mensual.20260701_v4_step_3_backfill_events_full_from_observations: el grueso. Trocea por parte de ClickHouse, no por partición, y el comentario explica por qué es la única granularidad segura:One todo per part keeps each INSERT bounded to a single ClickHouse part (single-digit GBs in most cases), which is the only granularity that’s safe to assume self-hoster hardware can chew through without OOM or memory-limit failures.
20260701_v4_step_4_backfill_events_full_from_dataset_run_items: recorre los dataset run items por cursor y escribe la raíz y sus descendientes. Lotes de 200, tope de 50.000 descendientes por elemento, ventana de 90 días.20260701_v4_step_5_drop_pid_tid_sorting_tables: tira las tablas de trabajo del paso 2.
Solo hay dos interruptores para las cinco. Los cuatro primeros comparten LANGFUSE_BACKGROUND_MIGRATION_V4_ENABLE_HISTORIC_BACKFILL, que viene en true, y el quinto tiene el suyo, LANGFUSE_BACKGROUND_MIGRATION_V4_DROP_PID_TID_SORTING_TABLES, que viene en false a propósito, para que las tablas de trabajo sigan ahí hasta que el operador confirme que el relleno salió bien.
Una sola migración a la vez en todo el clúster. El gestor coge un lock en Postgres, lo renueva con un latido cada 15 segundos y considera caducado un lock de más de 60 segundos, todo dentro de una transacción serializable. Añadir réplicas de worker no acelera el relleno ni un minuto. Esto es lo primero que sorprende a quien está acostumbrado a escalar horizontalmente para ir más rápido.
Son reanudables, y bastante mejor de lo que suele serlo esto. El progreso vive en un campo JSONB de la fila, y al reanudar el worker se reengancha a las consultas que dejó corriendo el worker anterior por su identificador de consulta, en vez de relanzarlas:
In-flight queries keep running server-side; the next run re-attaches to them via the persisted queryIds.
Sobre cuánto tardan, el código no dice nada. Ni los ficheros, ni el README de la carpeta, ni las migraciones. El único umbral temporal que aparece es de diseño: “A good threshold is something that takes more than 5 minutes to run”. Cualquier cifra de horas o días que leas por ahí, incluida la que yo mismo he repetido antes de comprobarlo, no está en el código.
El fallo que no bloquea
Este es el hallazgo que más cambia un runbook. Cuando una migración de fondo falla, el gestor la marca con failedAt y sigue. Y el predicado con el que elige la siguiente excluye precisamente las que tienen failedAt, de modo que una migración fallida se salta y no bloquea a las siguientes.
El propio código reconoce el agujero y explica el parche (worker/src/backgroundMigrations/utils/backfillBase.ts:107):
The
BackgroundMigrationManagerhas no dependency model — it runs every eligible row in name order and marks each finished/failed independently, so a failed upstream step does NOT by itself stop a downstream step from running on partial data. Each downstream step calls this from itsvalidate()[…] Because the next step in turn guards on its own predecessor, a single failure transitively halts the rest of the chain.
La guarda existe, pero solo en los pasos 3 y 4. Los pasos 1 y 2 no declaran predecesor. Un fallo del paso 1 no impide que el paso 2 arranque; lo que salva la situación es que el 3 sí comprueba al 2.
Consecuencia práctica: la única señal fiable de que el relleno va bien es mirar la tabla background_migrations en Postgres y comprobar que las cinco filas tienen finishedAt y ninguna tiene failedAt. Y para rearrancar una fallida hay que limpiar failedAt a mano y relanzar con --retry-failed, como dice el propio mensaje de error.
Dos modos de fallo concretos que merecen estar en el runbook:
- El paso 3 se rompe si ClickHouse fusiona una parte a mitad de camino. El mensaje lo dice todo: “Part … no longer active after processing — its rows are in a merged successor part that is not in this run’s todo list. Clear failedAt and re-run with state.chunksLoaded=false”. La recuperación es manual, editando el JSONB.
- El paso 4 se para en seco por un solo trace gigante, si supera los 50.000 descendientes. Las tres salidas que ofrece el error son subir el tope, saltar el elemento (y perder ese trace entero) o arreglarlo a mano por SQL.
Y una asimetría que hay que saber antes de comparar datos: el relleno histórico no copia los metadatos de la traza a los spans hijos, a propósito, mientras que el camino en vivo sí los concatena. El histórico y lo nuevo no son idénticos.
De dónde salen los diez minutos
La API v3 avisa de que sus datos van con unos diez minutos de retraso (deprecations.ts:16):
Data on this API is delayed by about 10 minutes. Real-time is only OpenTelemetry writes plus v2 observations and v2 metrics reads.
Ese retraso es la suma de cuatro cosas, todas visibles en el código:
- La tabla de paso particiona por intervalos de tres minutos.
- Una partición se da por cerrada cuatro minutos después de crearse: “We ’lock’ partitions 4min after their creation, i.e. the 15:00:00 partition should stop receiving updates at 15:04:00.”
- El cron de propagación corre cada minuto y solo mira particiones más viejas que
LANGFUSE_EXPERIMENT_EVENT_PROPAGATION_PARTITION_DELAY_MINUTES, que vale 10 por defecto. - Ese cron tiene concurrencia global de uno en todo el clúster y procesa una sola partición por invocación, así que si hay atasco el retraso crece en vez de recuperarse.
El modo de quitarse el retraso de encima no es tocar esa variable, es que los clientes escriban por OTLP con escritura directa, que es la ruta en tiempo real. La cabecera que la activa es x-langfuse-ingestion-version: 4, la mandan solos los SDK de Python desde la 4.0.0 y de JavaScript desde la 5.0.0, y un valor mayor que 4 se rechaza con 400.
El TTL de cuarenta y ocho horas
Esta es la que da miedo, y no aparece en ninguna guía de migración.
La tabla de paso tiene TTL toDateTime(s3_first_seen_timestamp) + INTERVAL 48 HOUR con ttl_only_drop_parts = 1. Si el propagador se para más de cuarenta y ocho horas, un puente largo por ejemplo, esas particiones se borran solas. Los datos ya están en las tablas legacy, así que la aplicación en modo dual sigue funcionando, pero el hueco en events_full es permanente, y no hay ningún código que lo detecte ni que lo repare. La sonda de salud detecta atascado, no perdido.
La defensa es una alerta, y existe para esto. El endpoint de salud del worker acepta ?failIfEventPropagationStuck=true y devuelve 503 si el latido del propagador lleva sin refrescarse más de LANGFUSE_EVENT_PROPAGATION_STUCK_THRESHOLD_MINUTES, que vale 35 por defecto. El comentario trae una condición que hay que leer antes de ponerlo en una sonda de Kubernetes:
Probes using this flag MUST set
initialDelaySeconds >= 60s(one cron cycle) […] A shorter delay can crash-loop the very restart this check triggers.
Otro detalle del mismo mecanismo: el cursor de la propagación vive en Redis, no en Postgres. Si Redis se vacía, el cursor vuelve a cero y se reprocesan todas las particiones vivas de la tabla de paso.
Qué se puede deshacer y qué no
Las migraciones de bajada de ClickHouse existen, y son honestas sobre lo que hacen. La de la 0039 avisa en mayúsculas:
WARNING: destructive rollback. The up migration uses IF NOT EXISTS, so this table may predate this migration […] Rolling back drops that data regardless of which path created the table.
Las de las tablas borradas recrean el esquema y nada más: “Any data held in event_log before the up migration dropped it is NOT restored.”
Pero el punto de no retorno de verdad no está en ClickHouse, está en Postgres. Hay una migración de Prisma llamada 20260723150000_drop_legacy_tracing_tables que hace exactamente lo que dice:
DROP TABLE IF EXISTS "traces";
DROP TABLE IF EXISTS "observations";
DROP TABLE IF EXISTS "scores";
Prisma Migrate no tiene migraciones de bajada. Se aplica y ya está.
Resumiendo la reversibilidad real: el modo de escritura se puede bajar de events_only a dual cuando quieras, y ese es el botón de pánico. Lo que no se recupera es lo que no se escribió mientras estuviste arriba: las tablas legacy no reciben nada retroactivamente. Y el paso 5 de la cadena, el que borra las tablas de trabajo, viene desactivado precisamente para que puedas hacer forense si algo salió mal.
Una última nota sobre en qué terreno pisas. El comentario de la política de exportaciones deja caer algo para leer dos veces: “legacy-writes-disabled, which in practice only surfaces on self-hosted (Cloud does not run events_only)”. La ruta events_only es la que usarás tú y no la que usa el servicio gestionado del fabricante. Está menos rodada en producción que dual.
Cómo queda esto en código
El runbook mínimo, en variables:
# Paso 2: subir a v4 comportándose como v3
LANGFUSE_MIGRATION_V4_WRITE_MODE=legacy
LANGFUSE_MIGRATION_V4_NATIVE_OTEL_BEHAVIOUR=dual_write # "direct" aqui no arranca
LANGFUSE_BACKGROUND_MIGRATION_V4_ENABLE_HISTORIC_BACKFILL=false
# Paso 3: tercer carril. Los dos servicios, web y worker, a la vez.
LANGFUSE_MIGRATION_V4_WRITE_MODE=dual
LANGFUSE_BACKGROUND_MIGRATION_V4_ENABLE_HISTORIC_BACKFILL=true
LANGFUSE_EVENT_PROPAGATION_STUCK_THRESHOLD_MINUTES=35
# Paso 4: solo cuando las cinco filas de background_migrations tengan finishedAt
LANGFUSE_MIGRATION_V4_WRITE_MODE=events_only
LANGFUSE_MIGRATION_V4_ALLOW_PREVIEW_OPT_IN=true # sin esto no arranca
LANGFUSE_BACKGROUND_MIGRATION_V4_DROP_PID_TID_SORTING_TABLES=true # el ultimo de todos
Y la consulta que responde a la única pregunta que importa durante semanas:
SELECT name, started_at, finished_at, failed_at, failed_reason,
state->>'chunksLoaded' AS chunks
FROM background_migrations
WHERE name LIKE '20260701_v4_%'
ORDER BY name;
Checklist de migración
- Las migraciones de fondo de la v3 están todas terminadas antes de subir la imagen.
- Hay copia de seguridad de Postgres y de ClickHouse anterior al primer
DROP TABLE. - El modo de escritura vale lo mismo en el worker y en la web, y se cambia en los dos a la vez.
- Se ha subido a
legacyprimero, y se ha comprobado que el servicio sigue igual. - Hay una alerta sobre
?failIfEventPropagationStuck=true, coninitialDelaySecondsde al menos 60 segundos. - Alguien mira la tabla
background_migrationstodos los días mientras dure el relleno. - Se ha inventariado qué integraciones propias llaman a las veinticuatro rutas que van a devolver 404.
- Los SDK de los clientes están en Python 4.0.0 o JavaScript 5.0.0 como mínimo antes de pasar a
events_only. - El paso 5 de la cadena se activa el último, y solo después de verificar el relleno.
Trampas
El valor por defecto es el estado final. Una instalación v3 que arranque la v4 sin tocar la variable se pone en events_only y deja de escribir las tablas viejas desde el primer segundo.
Una migración de fondo fallida no bloquea a la siguiente. Y los dos primeros pasos de la cadena no declaran predecesor.
Más réplicas de worker no aceleran el relleno. Una migración a la vez en todo el clúster, por diseño.
Cuarenta y ocho horas de propagador parado es un agujero permanente. Sin detección y sin reparación.
Los índices nuevos no cubren los datos viejos. Las búsquedas históricas escanean hasta que alguien materialice los índices a mano.
En clúster, cada ALTER espera a todas las réplicas. Una réplica caída cuelga la migración.
El rechazo de la ingesta clásica es por evento. Un lote mixto devuelve 207 y se pierde la mitad sin que nadie mire el cuerpo de la respuesta.
El histórico y el camino en vivo no producen exactamente lo mismo. Los metadatos de traza no bajan a los hijos en el relleno.
La ruta que vas a usar no es la que usa el fabricante. Cloud no corre events_only.
La serie: los diez artículos
- Qué entra en una traza: modelo de datos de la versión 4, límites, precedencias, scores, enmascarado e índices.
- Poner LangGraph delante: la instrumentación de una plataforma agéntica y el coste medido de un turno.
- Dos agentes conocidos instrumentados: Open Deep Research y GPT Researcher, con las cifras medidas de una petición real.
- Migrar de la versión 3 a la 4 sin ventana (este artículo): los tres pasos del modo de escritura, las migraciones de fondo reanudables y dónde está el punto de no retorno del retroceso.
- Las colas del worker: el mapa de las treinta y nueve, qué pool dedicar a cada grupo, los interruptores por cola, el particionado y la concurrencia.
- Capacidad y coste real de ClickHouse: cómo medir los bytes por observación con las tablas del sistema, la diferencia entre la tabla completa y la de listados, y el coste de fusión de los índices de texto completo.
- Retención, borrado y protección de datos: por qué un borrado no libera disco, el limpiador de máscaras que viene desactivado, la cola de borrados pendientes y el ciclo de vida de S3 que hay que implementar a mano.
- Copias de seguridad y recuperación cruzada: orden de restauración entre Postgres, ClickHouse y el almacenamiento de objetos, qué rompe cada desajuste, y hasta dónde llega la reproducción de eventos.
- Runbook de saturación: qué alertar de las métricas de cola, las sondas de atasco, el drenado por el endpoint de preparación y la cola de mensajes fallidos.
- Sacar los datos fuera: la integración de almacenamiento de objetos a Parquet, las exportaciones por lotes y la API de métricas, para montar el lago de datos.
Ver también
- Langfuse v4: qué entra de verdad en una traza: el modelo de datos al que lleva esta migración.
- Langfuse por dentro: la arquitectura de la versión 3, que es de donde se sale.
- LiteLLM y Langfuse: el par operativo: qué pasa con el gateway durante el cambio.
- Dos agentes conocidos instrumentados: el volumen que va a tener que tragar la instalación migrada.
Fuentes
- Código de Langfuse 4.35.0, commit
2ee1908del 14 de septiembre de 2026:packages/shared/src/env.ts,worker/src/env.ts,web/src/env.mjs,web/src/features/public-api/server/deprecations.ts,web/src/pages/api/public/ingestion.ts,web/src/features/public-api/server/createAuthedProjectAPIRoute.ts,worker/src/backgroundMigrations/,worker/src/features/eventPropagation/,packages/shared/clickhouse/migrations/canonical/,packages/shared/prisma/migrations/. - Compatibilidad y migración de Langfuse, consultado el 14 de septiembre de 2026.
- Migración a v4 de la integración OpenTelemetry, consultado el 14 de septiembre de 2026.