fix: treat blank tenant fanout toggle as disabled

This commit is contained in:
Ravi Kumar L 2026-06-21 23:27:29 +02:00
parent 6f36df4d12
commit b17579ae40
10 changed files with 81 additions and 13 deletions

View file

@ -132,7 +132,7 @@ NODE_MAX_OLD_SPACE_SIZE=6144
# See otel/langfuse-fanout/README.md.
# LANGFUSE_FANOUT_ENABLED=false
# LANGFUSE_FANOUT_COLLECTOR_URL=http://langfuse-fanout-collector:4318
# Emergency switch: set false to keep central collector export but skip tenant trace/score export.
# Emergency switch: unset defaults enabled; set false or blank to keep central collector export but skip tenant trace/score export.
# LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED=true
# Langfuse Cloud base URL options: https://cloud.langfuse.com (EU),
# https://us.cloud.langfuse.com (US), https://jp.cloud.langfuse.com (JP),

View file

@ -5,7 +5,7 @@ services:
environment:
- LANGFUSE_FANOUT_ENABLED=true
- LANGFUSE_FANOUT_COLLECTOR_URL=http://langfuse-fanout-collector:4318
- LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED=${LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED:-true}
- LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED=${LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED-true}
- LANGFUSE_FANOUT_TENANT_DESTINATIONS=${LANGFUSE_FANOUT_TENANT_DESTINATIONS:-eu=https://cloud.langfuse.com,us=https://us.cloud.langfuse.com,jp=https://jp.cloud.langfuse.com,hipaa=https://hipaa.cloud.langfuse.com}
networks:
- default

View file

@ -5,7 +5,7 @@ services:
environment:
- LANGFUSE_FANOUT_ENABLED=true
- LANGFUSE_FANOUT_COLLECTOR_URL=http://langfuse-fanout-collector:4318
- LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED=${LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED:-true}
- LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED=${LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED-true}
- LANGFUSE_FANOUT_TENANT_DESTINATIONS=${LANGFUSE_FANOUT_TENANT_DESTINATIONS:-eu=https://cloud.langfuse.com,us=https://us.cloud.langfuse.com,jp=https://jp.cloud.langfuse.com,hipaa=https://hipaa.cloud.langfuse.com}
networks:
- default

View file

@ -64,9 +64,10 @@ When enabled, the chart also sets `LANGFUSE_FANOUT_ENABLED` and
`LANGFUSE_FANOUT_COLLECTOR_URL` for the LibreChat app unless those values are
already provided in `librechat.configEnv`.
Set `librechat.configEnv.LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED=false` to keep
central trace export flowing through the collector while disabling tenant trace
and score export.
Set `librechat.configEnv.LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED=false` or a
blank value to keep central trace export flowing through the collector while
disabling tenant trace and score export. When omitted, tenant export defaults to
enabled if tenant keys and a known destination are configured.
Langfuse tenant base URLs are selected from the startup-configured destination
map rendered into LibreChat and the collector. Tenant API keys can still be added

View file

@ -40,9 +40,10 @@ defined in this collector config.
- Tenant app configuration must set a Langfuse base URL matching one of the
startup destinations before tenant trace/score export is enabled; keys alone
are treated as central-only.
- `LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED=false` can be set on LibreChat as an
emergency switch to stop tenant trace and score export while keeping central
collector export active.
- `LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED=false` or a blank value can be set on
LibreChat as an emergency switch to stop tenant trace and score export while
keeping central collector export active. When omitted, tenant export defaults
to enabled if tenant keys and a known destination are configured.
- This supports Langfuse Cloud and self-hosted Langfuse as long as each allowed
tenant base URL is configured at LibreChat/collector startup. Runtime tenant
config selects from those known destinations; it does not inject arbitrary

View file

@ -1351,6 +1351,32 @@ describe('Langfuse run config', () => {
});
});
it('treats blank tenant fanout export env as disabled', async () => {
process.env.LANGFUSE_PUBLIC_KEY = 'pk-central';
process.env.LANGFUSE_SECRET_KEY = 'sk-central';
process.env.LANGFUSE_FANOUT_ENABLED = 'true';
process.env.LANGFUSE_FANOUT_COLLECTOR_URL = 'http://collector-from-env:4318';
process.env.LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED = ' ';
const callArgs = await callAndCaptureRunConfig({
tenantId: 'tenant-1',
appConfig: {
langfuse: {
publicKey: 'pk-tenant-1',
secretKey: 'sk-tenant-1',
baseUrl: 'https://cloud.langfuse.com',
},
} as AppConfig,
});
expect(callArgs.langfuse).toEqual({
deterministicTraceId: true,
baseUrl: 'http://collector-from-env:4318',
metadata: { 'librechat.tenant.id': 'tenant-1' },
tags: ['tenant:tenant-1'],
});
});
it('lets tenant fanout.enabled=false override deployment fanout env', async () => {
process.env.LANGFUSE_FANOUT_ENABLED = 'true';
process.env.LANGFUSE_FANOUT_COLLECTOR_URL = 'http://collector-from-env:4318';

View file

@ -1,7 +1,7 @@
import type { AppConfig } from '@librechat/data-schemas';
import type { RunConfig } from '@librechat/agents';
import { resolveLangfuseTenantDestination } from './tenantDestinations';
import { isFalseEnv, normalizeString } from './utils';
import { isEnabledUnlessBlankOrFalse, normalizeString } from './utils';
type LangfuseRunConfig = NonNullable<RunConfig['langfuse']>;
type LangfuseRunConfigWithTraceAttributes = LangfuseRunConfig & {
@ -11,7 +11,7 @@ const TENANT_EXPORT_ATTRIBUTE = 'librechat.langfuse.tenant_export.enabled';
const TENANT_DESTINATION_ATTRIBUTE = 'librechat.langfuse.destination';
function isTenantExportEnabled(): boolean {
return !isFalseEnv(process.env.LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED);
return isEnabledUnlessBlankOrFalse(process.env.LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED);
}
function mergeTraceMetadata(

View file

@ -1,5 +1,10 @@
import type { AppConfig } from '@librechat/data-schemas';
import { isFalseEnv, normalizeString, toBasicAuthorization } from './utils';
import {
isEnabledUnlessBlankOrFalse,
isFalseEnv,
normalizeString,
toBasicAuthorization,
} from './utils';
import { resolveLangfuseTenantDestination } from './tenantDestinations';
const DEFAULT_BASE_URL = 'https://cloud.langfuse.com';
@ -65,7 +70,7 @@ function getTenantScoreDestination(appConfig?: AppConfig): LangfuseScoreDestinat
if (!isTracingEnabled()) {
return undefined;
}
if (isFalseEnv(process.env.LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED)) {
if (!isEnabledUnlessBlankOrFalse(process.env.LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED)) {
return undefined;
}

View file

@ -363,6 +363,32 @@ describe('Langfuse feedback scores', () => {
);
});
it('treats blank tenant fanout export env as disabled for scores', async () => {
process.env.LANGFUSE_FANOUT_CENTRAL_AUTH_HEADER = 'Basic central-auth';
process.env.LANGFUSE_FANOUT_CENTRAL_BASE_URL = 'http://central-langfuse:3000';
process.env.LANGFUSE_FANOUT_TENANT_EXPORT_ENABLED = ' ';
const { sendFeedbackScore } = await loadFeedback();
await sendFeedbackScore({
traceId: 'trace-id',
feedback: { rating: 'thumbsUp' },
appConfig: appConfigWithLangfuse({
publicKey: 'tenant-public-key',
secretKey: 'tenant-secret-key',
baseUrl: 'https://cloud.langfuse.com',
}),
});
expect(getFetchMock()).toHaveBeenCalledTimes(1);
expect(getFetchMock()).toHaveBeenCalledWith(
'http://central-langfuse:3000/api/public/scores',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({ Authorization: 'Basic central-auth' }),
}),
);
});
it('deduplicates matching central and tenant score destinations', async () => {
process.env.LANGFUSE_FANOUT_CENTRAL_AUTH_HEADER = getTenantAuthorization();
process.env.LANGFUSE_FANOUT_CENTRAL_BASE_URL = 'http://shared-langfuse:3000';

View file

@ -6,6 +6,15 @@ export function isFalseEnv(value?: string): boolean {
return value != null && ['0', 'false', 'no', 'off'].includes(value.trim().toLowerCase());
}
export function isEnabledUnlessBlankOrFalse(value?: string): boolean {
if (value == null) {
return true;
}
const normalized = value.trim().toLowerCase();
return normalized !== '' && !['0', 'false', 'no', 'off'].includes(normalized);
}
export function toBasicAuthorization(publicKey: string, secretKey: string): string {
return `Basic ${Buffer.from(`${publicKey}:${secretKey}`).toString('base64')}`;
}