Compare commits

...
8 Commits
Author SHA1 Message Date
Marc Klingen 898cca56f9 chore: release v2.78.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (node20, pg12) (push) Waiting to run
CI/CD / tests-web (node20, pg15) (push) Waiting to run
CI/CD / tests-worker (node20, pg12) (push) Waiting to run
CI/CD / tests-worker (node20, pg15) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-09-04 17:29:47 +02:00
Steffen SchmitzandGitHub 83e052e769 feat: migrate web api to new logger (#3230) 2024-09-04 17:02:26 +02:00
Steffen SchmitzandGitHub b5f19771fa feat: migrate shared package to new logger (#3229) 2024-09-04 16:43:36 +02:00
Marc KlingenandGitHub 644390183b fix: rename LANGFUSE_PROVISION_ to LANGFUSE_INIT_ ahead of changelog release (#3228) 2024-09-04 15:06:47 +02:00
Steffen SchmitzandGitHub 4d8e9003fe feat: add structured logging option (#3221) 2024-09-04 14:53:42 +02:00
Marc KlingenandGitHub 651c46a270 docs: add provisioning envs to .env.prod.example (#3227) 2024-09-04 14:24:53 +02:00
Max DeichmannandGitHub 6b127bced0 chore: add instrumentation to ingestion zod parsing (#3224) 2024-09-04 10:08:05 +00:00
Steffen SchmitzandGitHub e3a837b561 feat: add new /ready endpoint to separate liveness from readiness (#3222) 2024-09-04 11:53:41 +02:00
88 changed files with 1164 additions and 1165 deletions
+15 -2
View File
@@ -39,6 +39,10 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# LANGFUSE_DEFAULT_PROJECT_ID=
# LANGFUSE_DEFAULT_PROJECT_ROLE=
# Logging, optional
# LANGFUSE_LOG_LEVEL=info
# LANGFUSE_LOG_FORMAT=text
# Enable experimental features, optional
# LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=true
@@ -97,6 +101,17 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# The page size can be adjusted if needed to optimize performance
# DB_EXPORT_PAGE_SIZE=1000
# Automated provisioning of default resources
# LANGFUSE_INIT_ORG_ID=org-id
# LANGFUSE_INIT_ORG_NAME=org-name
# LANGFUSE_INIT_PROJECT_ID=project-id
# LANGFUSE_INIT_PROJECT_NAME=project-name
# LANGFUSE_INIT_PROJECT_PUBLIC_KEY=pk-1234567890
# LANGFUSE_INIT_PROJECT_SECRET_KEY=sk-1234567890
# LANGFUSE_INIT_USER_EMAIL=user@example.com
# LANGFUSE_INIT_USER_NAME=User Name
# LANGFUSE_INIT_USER_PASSWORD=password
### START Enterprise Edition Configuration
@@ -145,7 +160,6 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# NEXT_SENTRY_PROJECT=
# SENTRY_AUTH_TOKEN=
# SENTRY_CSP_REPORT_URI=
# LANGFUSE_WORKER_BETTERSTACK_TOKEN=
# Cloudflare Turnstile
@@ -201,7 +215,6 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE=
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
# LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS=
# LANGFUSE_LOG_LEVEL=
# LANGFUSE_LEGACY_INGESTION_WORKER_CONCURRENCY=
# LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
# QUEUE_CONSUMER_LEGACY_INGESTION_QUEUE_IS_ENABLED="true"
+9 -9
View File
@@ -121,15 +121,15 @@ jobs:
- name: Start Langfuse
run: (pnpm run start&)
env:
LANGFUSE_PROVISION_ORG_ID: "seed-org-id"
LANGFUSE_PROVISION_ORG_NAME: "Seed Org"
LANGFUSE_PROVISION_PROJECT_ID: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a"
LANGFUSE_PROVISION_PROJECT_NAME: "Seed Project"
LANGFUSE_PROVISION_PROJECT_PUBLIC_KEY: "pk-lf-1234567890"
LANGFUSE_PROVISION_PROJECT_SECRET_KEY: "sk-lf-1234567890"
LANGFUSE_PROVISION_USER_EMAIL: "demo@langfuse.com"
LANGFUSE_PROVISION_USER_NAME: "Demo User"
LANGFUSE_PROVISION_USER_PASSWORD: "password"
LANGFUSE_INIT_ORG_ID: "seed-org-id"
LANGFUSE_INIT_ORG_NAME: "Seed Org"
LANGFUSE_INIT_PROJECT_ID: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a"
LANGFUSE_INIT_PROJECT_NAME: "Seed Project"
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: "pk-lf-1234567890"
LANGFUSE_INIT_PROJECT_SECRET_KEY: "sk-lf-1234567890"
LANGFUSE_INIT_USER_EMAIL: "demo@langfuse.com"
LANGFUSE_INIT_USER_NAME: "Demo User"
LANGFUSE_INIT_USER_PASSWORD: "password"
- name: run tests
run: pnpm --filter=web run test
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.77.0",
"version": "2.78.0",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
+3 -2
View File
@@ -79,6 +79,7 @@
"nodemailer": "^6.9.13",
"prisma-extension-kysely": "^2.1.0",
"uuid": "^9.0.1",
"winston": "^3.14.2",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.23.2"
},
@@ -107,7 +108,7 @@
"vitest": "^1.5.3"
},
"peerDependencies": {
"@types/react": "^18.2.79",
"react": "^18.0.0"
"@types/react": "~18.2.79",
"react": "~18.2.0"
}
}
+46 -46
View File
@@ -12,7 +12,7 @@ import { parseArgs } from "node:util";
import { chunk } from "lodash";
import { v4 } from "uuid";
import { ModelUsageUnit } from "../src";
import { getDisplaySecretKey, hashSecretKey } from "../src/server";
import { getDisplaySecretKey, hashSecretKey, logger } from "../src/server";
import { encrypt } from "../src/encryption";
import { redis } from "../src/server/redis/redis";
@@ -265,11 +265,11 @@ async function main() {
project1,
project2,
promptIds,
configIdsAndNames
configIdsAndNames,
);
console.log(
`Seeding ${traces.length} traces, ${observations.length} observations, and ${scores.length} scores`
logger.info(
`Seeding ${traces.length} traces, ${observations.length} observations, and ${scores.length} scores`,
);
await uploadObjects(
@@ -278,7 +278,7 @@ async function main() {
scores,
sessions,
events,
comments
comments,
);
// If openai key is in environment, add it to the projects LLM API keys
@@ -295,8 +295,8 @@ async function main() {
},
});
} else {
console.warn(
"No OPENAI_API_KEY found in environment. Skipping seeding LLM API key."
logger.warn(
"No OPENAI_API_KEY found in environment. Skipping seeding LLM API key.",
);
}
@@ -431,7 +431,7 @@ async function main() {
for (const datasetItemId of datasetItemIds) {
const relevantObservations = observations.filter(
(o) => o.projectId === project2.id
(o) => o.projectId === project2.id,
);
const observation =
relevantObservations[
@@ -457,13 +457,13 @@ main()
.then(async () => {
await prisma.$disconnect();
redis?.disconnect();
console.log("Disconnected from postgres and redis");
logger.info("Disconnected from postgres and redis");
})
.catch(async (e) => {
console.error(e);
logger.error(e);
await prisma.$disconnect();
redis?.disconnect();
console.log("Disconnected from postgres and redis");
logger.info("Disconnected from postgres and redis");
process.exit(1);
});
@@ -473,7 +473,7 @@ async function uploadObjects(
scores: Prisma.ScoreCreateManyInput[],
sessions: Prisma.TraceSessionCreateManyInput[],
events: Prisma.ObservationCreateManyInput[],
comments: Prisma.CommentCreateManyInput[]
comments: Prisma.CommentCreateManyInput[],
) {
let promises: Prisma.PrismaPromise<unknown>[] = [];
@@ -487,14 +487,14 @@ async function uploadObjects(
},
create: chunk[0]!,
update: {},
})
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
console.log(
`Seeding of Sessions ${((i + 1) / promises.length) * 100}% complete`
logger.info(
`Seeding of Sessions ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
@@ -505,13 +505,13 @@ async function uploadObjects(
promises.push(
prisma.trace.createMany({
data: chunk,
})
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
console.log(
`Seeding of Traces ${((i + 1) / promises.length) * 100}% complete`
logger.info(
`Seeding of Traces ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
@@ -521,14 +521,14 @@ async function uploadObjects(
promises.push(
prisma.observation.createMany({
data: chunk,
})
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
console.log(
`Seeding of Observations ${((i + 1) / promises.length) * 100}% complete`
logger.info(
`Seeding of Observations ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
@@ -538,14 +538,14 @@ async function uploadObjects(
promises.push(
prisma.observation.createMany({
data: chunk,
})
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
console.log(
`Seeding of Events ${((i + 1) / promises.length) * 100}% complete`
logger.info(
`Seeding of Events ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
@@ -555,13 +555,13 @@ async function uploadObjects(
promises.push(
prisma.score.createMany({
data: chunk,
})
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
console.log(
`Seeding of Scores ${((i + 1) / promises.length) * 100}% complete`
logger.info(
`Seeding of Scores ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
@@ -571,13 +571,13 @@ async function uploadObjects(
promises.push(
prisma.comment.createMany({
data: chunk,
})
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
console.log(
`Seeding of Comments ${((i + 1) / promises.length) * 100}% complete`
logger.info(
`Seeding of Comments ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
@@ -598,7 +598,7 @@ function createObjects(
dataType: ScoreDataType;
categories: ConfigCategory[] | null;
}[]
>
>,
) {
const traces: Prisma.TraceCreateManyInput[] = [];
const observations: Prisma.ObservationCreateManyInput[] = [];
@@ -612,7 +612,7 @@ function createObjects(
// print progress to console with a progress bar that refreshes every 10 iterations
// random date within last 90 days, with a linear bias towards more recent dates
const traceTs = new Date(
Date.now() - Math.floor(Math.random() ** 1.5 * 90 * 24 * 60 * 60 * 1000)
Date.now() - Math.floor(Math.random() ** 1.5 * 90 * 24 * 60 * 60 * 1000),
);
const envTag = envTags[Math.floor(Math.random() * envTags.length)];
@@ -753,11 +753,11 @@ function createObjects(
for (let j = 0; j < Math.floor(Math.random() * 10) + 1; j++) {
// add between 1 and 30 ms to trace timestamp
const spanTsStart = new Date(
traceTs.getTime() + Math.floor(Math.random() * 30)
traceTs.getTime() + Math.floor(Math.random() * 30),
);
// random duration of upto 5000ms
const spanTsEnd = new Date(
spanTsStart.getTime() + Math.floor(Math.random() * 5000)
spanTsStart.getTime() + Math.floor(Math.random() * 5000),
);
const span = {
@@ -792,22 +792,22 @@ function createObjects(
const generationTsStart = new Date(
spanTsStart.getTime() +
Math.floor(
Math.random() * (spanTsEnd.getTime() - spanTsStart.getTime())
)
Math.random() * (spanTsEnd.getTime() - spanTsStart.getTime()),
),
);
const generationTsEnd = new Date(
generationTsStart.getTime() +
Math.floor(
Math.random() *
(spanTsEnd.getTime() - generationTsStart.getTime())
)
(spanTsEnd.getTime() - generationTsStart.getTime()),
),
);
// somewhere in the middle
const generationTsCompletionStart = new Date(
generationTsStart.getTime() +
Math.floor(
(generationTsEnd.getTime() - generationTsStart.getTime()) / 3
)
(generationTsEnd.getTime() - generationTsStart.getTime()) / 3,
),
);
const promptTokens = Math.floor(Math.random() * 1000) + 300;
@@ -828,7 +828,7 @@ function createObjects(
const promptId =
promptIds.get(projectId)![
Math.floor(
Math.random() * Math.floor(promptIds.get(projectId)!.length / 2)
Math.random() * Math.floor(promptIds.get(projectId)!.length / 2),
)
];
@@ -910,8 +910,8 @@ function createObjects(
const eventTs = new Date(
spanTsStart.getTime() +
Math.floor(
Math.random() * (spanTsEnd.getTime() - spanTsStart.getTime())
)
Math.random() * (spanTsEnd.getTime() - spanTsStart.getTime()),
),
);
events.push({
@@ -933,7 +933,7 @@ function createObjects(
}
// find unique sessions by id and projectid
const uniqueSessions: Prisma.TraceSessionCreateManyInput[] = Array.from(
new Set(sessions.map((session) => JSON.stringify(session)))
new Set(sessions.map((session) => JSON.stringify(session))),
).map((session) => JSON.parse(session) as Prisma.TraceSessionCreateManyInput);
return {
@@ -954,7 +954,7 @@ async function generatePromptsForProject(projects: Project[]) {
projects.map(async (project) => {
const promptIdsForProject = await generatePrompts(project);
promptIds.set(project.id, promptIdsForProject);
})
}),
);
return promptIds;
}
@@ -1140,7 +1140,7 @@ async function generateConfigsForProject(projects: Project[]) {
projects.map(async (project) => {
const configNameAndId = await generateConfigs(project);
projectIdsToConfigs.set(project.id, configNameAndId);
})
}),
);
return projectIdsToConfigs;
}
+28 -7
View File
@@ -1,7 +1,7 @@
// This file exports the prisma db connection, the Prisma Object, and the Typescript types.
// This is not imported in the index.ts file of this package, as we must not import this into FE code.
import { PrismaClient } from "@prisma/client";
import { Prisma, PrismaClient } from "@prisma/client";
import { env } from "process";
import kyselyExtension from "prisma-extension-kysely";
import {
@@ -11,17 +11,38 @@ import {
PostgresQueryCompiler,
} from "kysely";
import { DB } from ".";
import { logger } from "./server";
// Instantiated according to the Prisma documentation
// https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices
const prismaClientSingleton = () => {
return new PrismaClient({
log:
env.NODE_ENV === "development"
? ["query", "error", "warn"]
: ["error", "warn"],
const client = new PrismaClient<
Prisma.PrismaClientOptions,
"warn" | "error" | "query"
>({
log: [
{ emit: "event", level: "query" },
{ emit: "event", level: "error" },
{ emit: "event", level: "warn" },
],
});
if (env.NODE_ENV === "development") {
client.$on("query", (event) => {
logger.info(`prisma:query ${event.query}, ${event.duration}ms`);
});
}
client.$on("warn", (event) => {
logger.warn(`prisma:warn ${event.message}`);
});
client.$on("error", (event) => {
logger.error(`prisma:error ${event.message}`);
});
return client;
};
const kyselySingleton = (prismaClient: PrismaClient) => {
@@ -38,7 +59,7 @@ const kyselySingleton = (prismaClient: PrismaClient) => {
createQueryCompiler: () => new PostgresQueryCompiler(),
},
}),
})
}),
);
};
declare global {
+5 -1
View File
@@ -21,7 +21,7 @@ const EnvSchema = z.object({
.string()
.length(
64,
"ENCRYPTION_KEY must be 256 bits, 64 string characters in hex format, generate via: openssl rand -hex 32"
"ENCRYPTION_KEY must be 256 bits, 64 string characters in hex format, generate via: openssl rand -hex 32",
)
.optional(),
LANGFUSE_CACHE_PROMPT_ENABLED: z.enum(["true", "false"]).default("false"),
@@ -39,6 +39,10 @@ const EnvSchema = z.object({
.positive()
.default(60 * 10),
SALT: z.string().optional(), // used by components imported by web package
LANGFUSE_LOG_LEVEL: z
.enum(["trace", "debug", "info", "warn", "error", "fatal"])
.optional(),
LANGFUSE_LOG_FORMAT: z.enum(["text", "json"]).default("text"),
});
export const env = EnvSchema.parse(process.env);
@@ -83,13 +83,13 @@ export const ScoreBodyWithoutConfig = z.discriminatedUnion("dataType", [
z.object({
value: z.number(),
dataType: z.literal("NUMERIC"),
})
}),
),
BaseScoreBody.merge(
z.object({
value: z.string(),
dataType: z.literal("CATEGORICAL"),
})
}),
),
BaseScoreBody.merge(
z.object({
@@ -97,7 +97,7 @@ export const ScoreBodyWithoutConfig = z.discriminatedUnion("dataType", [
message: "Value must be either 0 or 1",
}),
dataType: z.literal("BOOLEAN"),
})
}),
),
]);
@@ -161,7 +161,7 @@ export const ScorePropsAgainstConfig = z.union([
*/
export const filterAndValidateDbScoreList = (
scores: Score[],
onParseError?: (error: z.ZodError) => void
onParseError?: (error: z.ZodError) => void,
): APIScore[] =>
scores.reduce((acc, ts) => {
const result = APIScoreSchema.safeParse(ts);
@@ -198,14 +198,14 @@ export const PostScoresBody = z.discriminatedUnion("dataType", [
value: z.number(),
dataType: z.literal("NUMERIC"),
configId: z.string().nullish(),
})
}),
),
BaseScoreBody.merge(
z.object({
value: z.string(),
dataType: z.literal("CATEGORICAL"),
configId: z.string().nullish(),
})
}),
),
BaseScoreBody.merge(
z.object({
@@ -215,14 +215,14 @@ export const PostScoresBody = z.discriminatedUnion("dataType", [
}),
dataType: z.literal("BOOLEAN"),
configId: z.string().nullish(),
})
}),
),
BaseScoreBody.merge(
z.object({
value: z.union([z.string(), z.number()]),
dataType: z.undefined(),
configId: z.string().nullish(),
})
}),
),
]);
@@ -256,7 +256,7 @@ const LegacyGetScoreResponseDataV1 = z.intersection(
trace: z.object({
userId: z.string().nullish(),
}),
})
}),
);
export const GetScoresResponse = z.object({
data: z.array(LegacyGetScoreResponseDataV1),
@@ -265,7 +265,7 @@ export const GetScoresResponse = z.object({
export const legacyFilterAndValidateV1GetScoreList = (
scores: unknown[],
onParseError?: (error: z.ZodError) => void
onParseError?: (error: z.ZodError) => void,
): z.infer<typeof LegacyGetScoreResponseDataV1>[] =>
scores.reduce(
(acc: z.infer<typeof LegacyGetScoreResponseDataV1>[], ts) => {
@@ -278,7 +278,7 @@ export const legacyFilterAndValidateV1GetScoreList = (
}
return acc;
},
[] as z.infer<typeof LegacyGetScoreResponseDataV1>[]
[] as z.infer<typeof LegacyGetScoreResponseDataV1>[],
);
// GET /scores/{scoreId}
-3
View File
@@ -1,13 +1,10 @@
export * from "./constants";
export * from "./queries";
export * from "./interfaces/filters";
export * from "./interfaces/orderBy";
export * from "./interfaces/cloudConfigSchema";
export * from "./interfaces/parseDbOrg";
export * from "./tableDefinitions";
export * from "./types";
export * from "./filterToPrisma";
export * from "./orderByToPrisma";
export * from "./tracesTable";
export * from "./server/auth/apiKeys";
export * from "./observationsTable";
@@ -1,8 +1,9 @@
import { Prisma } from "@prisma/client";
import { ColumnDefinition, type TableNames } from "./tableDefinitions";
import { FilterState } from "./types";
import { filterOperators, timeFilter } from "./interfaces/filters";
import { ColumnDefinition, type TableNames } from "../tableDefinitions";
import { FilterState } from "../types";
import { filterOperators, timeFilter } from "../interfaces/filters";
import { z } from "zod";
import { logger } from "./index";
const operatorReplacements = {
"any of": "IN",
@@ -25,7 +26,7 @@ const arrayOperatorReplacements = {
export function tableColumnsToSqlFilterAndPrefix(
filters: FilterState,
tableColumns: ColumnDefinition[],
table: TableNames
table: TableNames,
): Prisma.Sql {
const sql = tableColumnsToSqlFilter(filters, tableColumns, table);
if (sql === Prisma.empty) {
@@ -41,17 +42,17 @@ export function tableColumnsToSqlFilterAndPrefix(
export function tableColumnsToSqlFilter(
filters: FilterState,
tableColumns: ColumnDefinition[],
table: TableNames
table: TableNames,
): Prisma.Sql {
const internalFilters = filters.map((filter) => {
// Get column definition to map column to internal name, e.g. "t.id"
const col = tableColumns.find(
(c) =>
// TODO: Only use id instead of name
c.name === filter.column || c.id === filter.column
c.name === filter.column || c.id === filter.column,
);
if (!col) {
console.error("Invalid filter column", filter.column);
logger.error("Invalid filter column", filter.column);
throw new Error("Invalid filter column: " + filter.column);
}
const colPrisma = Prisma.raw(col.internal);
@@ -70,13 +71,13 @@ export function tableColumnsToSqlFilter(
? Prisma.raw(
arrayOperatorReplacements[
filter.operator as keyof typeof arrayOperatorReplacements
]
],
)
: filter.operator in operatorReplacements
? Prisma.raw(
operatorReplacements[
filter.operator as keyof typeof operatorReplacements
]
],
)
: Prisma.raw(filter.operator); //checked by zod
@@ -96,13 +97,13 @@ export function tableColumnsToSqlFilter(
break;
case "stringOptions":
valuePrisma = Prisma.sql`(${Prisma.join(
filter.value.map((v) => Prisma.sql`${v}`)
filter.value.map((v) => Prisma.sql`${v}`),
)})`;
break;
case "arrayOptions":
valuePrisma = Prisma.sql`ARRAY[${Prisma.join(
filter.value.map((v) => Prisma.sql`${v}`),
", "
", ",
)}] `;
break;
@@ -122,12 +123,12 @@ export function tableColumnsToSqlFilter(
filter.type === "string" || filter.type === "stringObject"
? [
["contains", "does not contain", "ends with"].includes(
filter.operator
filter.operator,
)
? Prisma.raw("'%' || ")
: Prisma.empty,
["contains", "does not contain", "starts with"].includes(
filter.operator
filter.operator,
)
? Prisma.raw(" || '%'")
: Prisma.empty,
@@ -151,7 +152,7 @@ export function tableColumnsToSqlFilter(
const castValueToPostgresTypes = (
column: ColumnDefinition,
table: TableNames
table: TableNames,
) => {
return column.name === "type" &&
(table === "observations" ||
@@ -167,7 +168,7 @@ const dateOperators = filterOperators["datetime"];
export const datetimeFilterToPrismaSql = (
safeColumn: string,
operator: (typeof dateOperators)[number],
value: Date
value: Date,
) => {
if (!dateOperators.includes(operator)) {
throw new Error("Invalid operator: " + operator);
@@ -177,12 +178,12 @@ export const datetimeFilterToPrismaSql = (
}
return Prisma.sql`AND ${Prisma.raw(safeColumn)} ${Prisma.raw(
operator
operator,
)} ${value}::timestamp with time zone at time zone 'UTC'`;
};
export const datetimeFilterToPrisma = (
timestampFilter: z.infer<typeof timeFilter>
timestampFilter: z.infer<typeof timeFilter>,
) => {
const prismaTimestampFilter =
timestampFilter.operator === ">="
+4
View File
@@ -25,4 +25,8 @@ export * from "./auth/types";
export * from "./ingestion/legacy/index";
export * from "./queues";
export * from "./ingestion/legacy/EventProcessor";
export * from "./orderByToPrisma";
export * from "./filterToPrisma";
export * from "./instrumentation";
export * from "./logger";
export * from "./queries";
@@ -18,12 +18,13 @@ import { mergeJson } from "../../../utils/json";
import { jsonSchema } from "../../../utils/zod";
import { prisma } from "../../../db";
import { LegacyIngestionAccessScope } from ".";
import { logger } from "../../logger";
export interface EventProcessor {
auth(apiScope: LegacyIngestionAccessScope): void;
process(
apiScope: LegacyIngestionAccessScope
apiScope: LegacyIngestionAccessScope,
): Promise<Trace | Observation | Score> | undefined;
}
@@ -39,7 +40,7 @@ export class ObservationProcessor implements EventProcessor {
calculateTokenDelegate: (p: {
model: Model;
text: unknown;
}) => number | undefined
}) => number | undefined,
) {
this.event = event;
this.calculateTokenDelegate = calculateTokenDelegate;
@@ -47,7 +48,7 @@ export class ObservationProcessor implements EventProcessor {
async convertToObservation(
apiScope: LegacyIngestionAccessScope,
existingObservation: Observation | null
existingObservation: Observation | null,
): Promise<{
id: string;
create: Prisma.ObservationUncheckedCreateInput;
@@ -77,7 +78,7 @@ export class ObservationProcessor implements EventProcessor {
!existingObservation
) {
throw new LangfuseNotFoundError(
`Observation with id ${this.event.id} not found`
`Observation with id ${this.event.id} not found`,
);
}
@@ -123,7 +124,7 @@ export class ObservationProcessor implements EventProcessor {
this.event.body,
this.calculateTokenDelegate,
internalModel ?? undefined,
existingObservation ?? undefined
existingObservation ?? undefined,
)
: [undefined, undefined];
@@ -159,7 +160,7 @@ export class ObservationProcessor implements EventProcessor {
const calculatedCosts = ObservationProcessor.calculateTokenCosts(
internalModel,
userProvidedTokenCosts,
tokenCounts
tokenCounts,
);
// merge metadata from existingObservation.metadata and metadata
@@ -167,7 +168,7 @@ export class ObservationProcessor implements EventProcessor {
existingObservation?.metadata
? jsonSchema.parse(existingObservation.metadata)
: undefined,
this.event.body.metadata ?? undefined
this.event.body.metadata ?? undefined,
);
const prompt =
@@ -187,8 +188,9 @@ export class ObservationProcessor implements EventProcessor {
: undefined;
// Only null if promptName and promptVersion are set but prompt is not found
if (prompt === null)
console.warn("Prompt not found for observation", this.event.body);
if (prompt === null) {
logger.warn("Prompt not found for observation", this.event.body);
}
const observationId = this.event.body.id ?? v4();
@@ -318,7 +320,7 @@ export class ObservationProcessor implements EventProcessor {
text: unknown;
}) => number | undefined,
model?: Model,
existingObservation?: Observation
existingObservation?: Observation,
) {
const newPromptTokens =
body.usage?.input ??
@@ -350,7 +352,7 @@ export class ObservationProcessor implements EventProcessor {
outputCost?: Decimal | null;
totalCost?: Decimal | null;
},
tokenCounts: { input?: number; output?: number; total?: number }
tokenCounts: { input?: number; output?: number; total?: number },
): {
inputCost?: Decimal | null;
outputCost?: Decimal | null;
@@ -367,7 +369,7 @@ export class ObservationProcessor implements EventProcessor {
totalCost:
userProvidedCosts.totalCost ??
(userProvidedCosts.inputCost ?? new Decimal(0)).add(
userProvidedCosts.outputCost ?? new Decimal(0)
userProvidedCosts.outputCost ?? new Decimal(0),
),
};
}
@@ -417,7 +419,7 @@ export class ObservationProcessor implements EventProcessor {
existingObservation.projectId !== apiScope.projectId
) {
throw new ForbiddenError(
`Access denied for observation creation ${existingObservation.projectId} `
`Access denied for observation creation ${existingObservation.projectId} `,
);
}
@@ -447,7 +449,7 @@ export class TraceProcessor implements EventProcessor {
}
async process(
apiScope: LegacyIngestionAccessScope
apiScope: LegacyIngestionAccessScope,
): Promise<Trace | Observation | Score> {
const { body } = this.event;
@@ -455,11 +457,8 @@ export class TraceProcessor implements EventProcessor {
const internalId = body.id ?? v4();
console.log(
"Trying to create trace, project ",
apiScope.projectId,
", id:",
internalId
logger.info(
`Trying to create trace, project ${apiScope.projectId}, id: ${internalId}`,
);
const existingTrace = await prisma.trace.findFirst({
@@ -470,7 +469,7 @@ export class TraceProcessor implements EventProcessor {
if (existingTrace && existingTrace.projectId !== apiScope.projectId) {
throw new ForbiddenError(
`Access denied for trace creation ${existingTrace.projectId}`
`Access denied for trace creation ${existingTrace.projectId}`,
);
}
@@ -478,7 +477,7 @@ export class TraceProcessor implements EventProcessor {
existingTrace?.metadata
? jsonSchema.parse(existingTrace.metadata)
: undefined,
body.metadata ?? undefined
body.metadata ?? undefined,
);
const mergedTags =
@@ -556,12 +555,12 @@ export class ScoreProcessor implements EventProcessor {
auth(apiScope: LegacyIngestionAccessScope) {
if (apiScope.accessLevel !== "scores" && apiScope.accessLevel !== "all")
throw new ForbiddenError(
`Access denied for score creation, ${apiScope.accessLevel}`
`Access denied for score creation, ${apiScope.accessLevel}`,
);
}
async process(
apiScope: LegacyIngestionAccessScope
apiScope: LegacyIngestionAccessScope,
): Promise<Trace | Observation | Score> {
const { body } = this.event;
@@ -579,7 +578,7 @@ export class ScoreProcessor implements EventProcessor {
});
if (existingScore && existingScore.projectId !== apiScope.projectId) {
throw new ForbiddenError(
`Access denied for score creation ${existingScore.projectId}`
`Access denied for score creation ${existingScore.projectId}`,
);
}
@@ -619,7 +618,7 @@ export class SdkLogProcessor implements EventProcessor {
process() {
try {
console.log("SDK Log", this.event);
logger.info("SDK Log", this.event);
return undefined;
} catch (error) {
return undefined;
@@ -8,10 +8,11 @@ import { IngestionUtils } from "../IngestionUtils";
import { IngestionEventType } from "../types";
import { redis } from "../../redis/redis";
import { env } from "../../../env";
import { logger } from "../../logger";
export async function enqueueIngestionEvents(
projectId: string,
events: IngestionEventType[]
events: IngestionEventType[],
) {
const ingestionFlushQueue = getIngestionFlushQueue();
@@ -31,8 +32,8 @@ export async function enqueueIngestionEvents(
event,
redis,
ingestionFlushQueue,
batchTimestamp
)
batchTimestamp,
),
);
}
@@ -44,11 +45,11 @@ async function enqueueSingleIngestionEvent(
event: IngestionEventType,
redis: Redis,
ingestionFlushQueue: IngestionFlushQueue,
batchTimestamp: string
batchTimestamp: string,
): Promise<void> {
if (!("id" in event.body && event.body.id)) {
console.warn(
`Received ingestion event without id: ${JSON.stringify(event)}`
logger.warn(
`Received ingestion event without id: ${JSON.stringify(event)}`,
);
return;
@@ -19,6 +19,7 @@ import { redis } from "../../redis/redis";
import { backOff } from "exponential-backoff";
import { Model } from "../../..";
import { enqueueIngestionEvents } from "./enqueueIngestionEvents";
import { logger } from "../../logger";
export type BatchResult = {
result: unknown;
@@ -49,9 +50,9 @@ type LegacyIngestionAuthHeaderVerificationResult =
export const handleBatch = async (
events: z.infer<typeof ingestionApiSchema>["batch"],
authCheck: LegacyIngestionAuthHeaderVerificationResult,
calculateTokenDelegate: (p: TokenCountInput) => number | undefined
calculateTokenDelegate: (p: TokenCountInput) => number | undefined,
) => {
console.log(`handling ingestion ${events.length} events`);
logger.info(`handling ingestion ${events.length} events`);
if (!authCheck.validKey) throw new UnauthorizedError(authCheck.error);
@@ -69,7 +70,7 @@ export const handleBatch = async (
return await handleSingleEvent(
singleEvent,
authCheck.scope,
calculateTokenDelegate
calculateTokenDelegate,
);
});
results.push({
@@ -79,7 +80,7 @@ export const handleBatch = async (
}); // Push each result into the array
} catch (error) {
// Handle or log the error if `handleSingleEvent` fails
console.error("Error handling event:", error);
logger.error("Error handling event:", error);
// Decide how to handle the error: rethrow, continue, or push an error object to results
// For example, push an error object:
errors.push({
@@ -93,9 +94,9 @@ export const handleBatch = async (
if (env.CLICKHOUSE_URL) {
try {
await enqueueIngestionEvents(authCheck.scope.projectId, events);
console.log(`Added ${events.length} ingestion events to queue`);
logger.info(`Added ${events.length} ingestion events to queue`);
} catch (err) {
console.error("Error adding ingestion events to queue", err);
logger.error("Error adding ingestion events to queue", err);
}
}
@@ -107,10 +108,10 @@ async function retry<T>(request: () => Promise<T>): Promise<T> {
numOfAttempts: env.LANGFUSE_ASYNC_INGESTION_PROCESSING === "true" ? 5 : 3,
retry: (e: Error, attemptNumber: number) => {
if (e instanceof UnauthorizedError || e instanceof ForbiddenError) {
console.log("not retrying auth error");
logger.info("not retrying auth error");
return false;
}
console.log(`retrying processing events ${attemptNumber}`);
logger.info(`retrying processing events ${attemptNumber}`);
return true;
},
});
@@ -122,7 +123,7 @@ const handleSingleEvent = async (
calculateTokenDelegate: (p: {
model: Model;
text: unknown;
}) => number | undefined
}) => number | undefined,
) => {
const { body } = event;
let restEvent = body;
@@ -137,8 +138,8 @@ const handleSingleEvent = async (
restEvent = rest;
}
console.log(
`handling single event ${event.id} of type ${event.type}: ${JSON.stringify({ body: restEvent })}`
logger.info(
`handling single event ${event.id} of type ${event.type}: ${JSON.stringify({ body: restEvent })}`,
);
const cleanedEvent = ingestionEvent.parse(cleanEvent(event));
@@ -159,7 +160,7 @@ const handleSingleEvent = async (
case eventTypes.GENERATION_UPDATE:
processor = new ObservationProcessor(
cleanedEvent,
calculateTokenDelegate
calculateTokenDelegate,
);
break;
case eventTypes.SCORE_CREATE: {
@@ -201,7 +202,7 @@ export function cleanEvent(obj: unknown): unknown {
}
export const isNotNullOrUndefined = <T>(
val?: T | null
val?: T | null,
): val is Exclude<T, null | undefined> => !isUndefinedOrNull(val);
export const isUndefinedOrNull = <T>(val?: T | null): val is undefined | null =>
@@ -209,7 +210,7 @@ export const isUndefinedOrNull = <T>(val?: T | null): val is undefined | null =>
export const sendToWorkerIfEnvironmentConfigured = async (
batchResults: BatchResult[],
projectId: string
projectId: string,
): Promise<void> => {
const traceEvents: TraceUpsertEventType[] = batchResults
.filter((result) => result.type === eventTypes.TRACE_CREATE) // we only have create, no update.
@@ -219,17 +220,17 @@ export const sendToWorkerIfEnvironmentConfigured = async (
"id" in result.result
? // ingestion API only gets traces for one projectId
{ traceId: result.result.id as string, projectId }
: null
: null,
)
.filter(isNotNullOrUndefined);
try {
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION && redis) {
console.log(`Sending ${traceEvents.length} events to worker via Redis`);
logger.info(`Sending ${traceEvents.length} events to worker via Redis`);
const queue = getTraceUpsertQueue();
if (!queue) {
console.error("TraceUpsertQueue not initialized");
logger.error("TraceUpsertQueue not initialized");
return;
}
@@ -239,7 +240,7 @@ export const sendToWorkerIfEnvironmentConfigured = async (
env.LANGFUSE_WORKER_PASSWORD &&
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
) {
console.log(`Sending ${traceEvents.length} events to worker via HTTP`);
logger.info(`Sending ${traceEvents.length} events to worker via HTTP`);
const body: EventBodyType = {
name: EventName.TraceUpsert,
payload: traceEvents,
@@ -253,7 +254,7 @@ export const sendToWorkerIfEnvironmentConfigured = async (
Authorization:
"Basic " +
Buffer.from(
"admin" + ":" + env.LANGFUSE_WORKER_PASSWORD
"admin" + ":" + env.LANGFUSE_WORKER_PASSWORD,
).toString("base64"),
},
body: JSON.stringify(body),
@@ -262,6 +263,6 @@ export const sendToWorkerIfEnvironmentConfigured = async (
}
}
} catch (error) {
console.error("Error sending events to worker", error);
logger.error("Error sending events to worker", error);
}
};
@@ -15,7 +15,7 @@ type ValidateAndInflateScoreParams = {
};
export async function validateAndInflateScore(
params: ValidateAndInflateScoreParams
params: ValidateAndInflateScoreParams,
): Promise<Score> {
const { body, projectId } = params;
@@ -29,7 +29,7 @@ export async function validateAndInflateScore(
if (!config || !validateDbScoreConfigSafe(config).success)
throw new LangfuseNotFoundError(
"The configId you provided does not match a valid config in this project"
"The configId you provided does not match a valid config in this project",
);
validateConfigAgainstBody(body, config as ValidatedScoreConfig);
@@ -47,7 +47,7 @@ export async function validateAndInflateScore(
if (!validation.success) {
throw new InvalidRequestError(
`Ingested score value type not valid against provided data type. Provide numeric values for numeric and boolean scores, and string values for categorical scores.`
`Ingested score value type not valid against provided data type. Provide numeric values for numeric and boolean scores, and string values for categorical scores.`,
);
}
@@ -62,7 +62,7 @@ function inferDataType(value: string | number): ScoreDataType {
function mapStringValueToNumericValue(
config: ValidatedScoreConfig,
label: string
label: string,
): number | null {
return (
config.categories?.find((category) => category.label === label)?.value ??
@@ -71,7 +71,7 @@ function mapStringValueToNumericValue(
}
function inflateScoreBody(
params: ValidateAndInflateScoreParams & { config?: ValidatedScoreConfig }
params: ValidateAndInflateScoreParams & { config?: ValidatedScoreConfig },
): Score {
const { body, projectId, scoreId, config } = params;
@@ -105,25 +105,25 @@ function inflateScoreBody(
function validateConfigAgainstBody(
body: any,
config: ValidatedScoreConfig
config: ValidatedScoreConfig,
): void {
const { maxValue, minValue, categories, dataType: configDataType } = config;
if (body.dataType && body.dataType !== configDataType) {
throw new InvalidRequestError(
`Data type mismatch based on config: expected ${configDataType}, got ${body.dataType}`
`Data type mismatch based on config: expected ${configDataType}, got ${body.dataType}`,
);
}
if (config.isArchived) {
throw new InvalidRequestError(
"Config is archived and cannot be used to create new scores. Please restore the config first."
"Config is archived and cannot be used to create new scores. Please restore the config first.",
);
}
if (config.name !== body.name) {
throw new InvalidRequestError(
`Name mismatch based on config: expected ${config.name}, got ${body.name}`
`Name mismatch based on config: expected ${config.name}, got ${body.name}`,
);
}
@@ -136,7 +136,7 @@ function validateConfigAgainstBody(
if (!dataTypeValidation.success) {
throw new InvalidRequestError(
`Ingested score body not valid against provided config data type.`
`Ingested score body not valid against provided config data type.`,
);
}
@@ -154,7 +154,7 @@ function validateConfigAgainstBody(
.join(", ");
throw new InvalidRequestError(
`Ingested score body not valid against provided config: ${errorDetails}`
`Ingested score body not valid against provided config: ${errorDetails}`,
);
}
}
+33
View File
@@ -0,0 +1,33 @@
import { env } from "../env";
import winston from "winston";
const getWinstonLogger = (
nodeEnv: "development" | "production" | "test",
minLevel = "info",
) => {
const textLoggerFormat = winston.format.combine(
winston.format.errors({ stack: true }),
winston.format.timestamp(),
winston.format.align(),
winston.format.printf((info) => {
const logMessage = `${info.timestamp} ${info.level} ${info.message}`;
return info.stack ? `${logMessage}\n${info.stack}` : logMessage;
}),
);
const jsonLoggerFormat = winston.format.combine(
winston.format.errors({ stack: true }),
winston.format.timestamp(),
winston.format.json(),
);
const format =
env.LANGFUSE_LOG_FORMAT === "text" ? textLoggerFormat : jsonLoggerFormat;
return winston.createLogger({
level: minLevel,
format: format,
transports: [new winston.transports.Console()],
});
};
export const logger = getWinstonLogger(env.NODE_ENV, env.LANGFUSE_LOG_LEVEL);
@@ -2,8 +2,9 @@ import { z } from "zod";
import { Prisma } from "@prisma/client";
import type { ColumnDefinition } from "./tableDefinitions/types";
import type { OrderByState } from "./interfaces/orderBy";
import type { ColumnDefinition } from "../tableDefinitions/types";
import type { OrderByState } from "../interfaces/orderBy";
import { logger } from "./logger";
/**
* Convert orderBy to SQL ORDER BY clause
@@ -13,7 +14,7 @@ import type { OrderByState } from "./interfaces/orderBy";
*/
export function orderByToPrismaSql(
orderBy: OrderByState,
tableColumns: ColumnDefinition[]
tableColumns: ColumnDefinition[],
): Prisma.Sql {
if (!orderBy) {
return Prisma.sql`ORDER BY t.timestamp DESC`;
@@ -22,11 +23,11 @@ export function orderByToPrismaSql(
const col = tableColumns.find(
// TODO: Only use id instead of name.
// It's less error-prone & decouples data fetching from the human-readable UI labels
(c) => c.name === orderBy.column || c.id === orderBy.column
(c) => c.name === orderBy.column || c.id === orderBy.column,
);
if (!col) {
console.log("Invalid filter column", orderBy.column);
logger.warn("Invalid filter column", orderBy.column);
throw new Error("Invalid filter column: " + orderBy.column);
}
@@ -34,12 +35,12 @@ export function orderByToPrismaSql(
const orderByOrder = z.enum(["ASC", "DESC"]);
const order = orderByOrder.safeParse(orderBy.order);
if (!order.success) {
console.log("Invalid order", orderBy.order);
logger.warn("Invalid order", orderBy.order);
throw new Error("Invalid order: " + orderBy.order);
}
// Both column and order are safe, can use raw SQL
return Prisma.raw(
`ORDER BY ${col.internal} ${order.data} ${col.nullable ? (orderBy.order === "DESC" ? "NULLS LAST" : "NULLS FIRST") : ""}`
`ORDER BY ${col.internal} ${order.data} ${col.nullable ? (orderBy.order === "DESC" ? "NULLS LAST" : "NULLS FIRST") : ""}`,
);
}
@@ -2,10 +2,10 @@ import { z } from "zod";
import { Prisma } from "@prisma/client";
import { tableColumnsToSqlFilterAndPrefix } from "../filterToPrisma";
import { singleFilter } from "../interfaces/filters";
import { orderBy } from "../interfaces/orderBy";
import { singleFilter } from "../../interfaces/filters";
import { orderBy } from "../../interfaces/orderBy";
import { orderByToPrismaSql } from "../orderByToPrisma";
import { sessionsViewCols } from "../tableDefinitions/index";
import { sessionsViewCols } from "../../tableDefinitions";
const GetSessionTableSQLParamsSchema = z.object({
projectId: z.string(),
@@ -22,7 +22,7 @@ export const createSessionsAllQuery = (
options?: {
ignoreOrderBy?: boolean; // used by session.metrics and session.all.totalCount
sessionIdList?: string[]; // used by session.metrics
}
},
): Prisma.Sql => {
const { projectId, filter, orderBy, page, limit } =
GetSessionTableSQLParamsSchema.parse(params);
@@ -30,7 +30,7 @@ export const createSessionsAllQuery = (
const filterCondition = tableColumnsToSqlFilterAndPrefix(
filter ?? [],
sessionsViewCols,
"sessions"
"sessions",
);
const orderByCondition = orderByToPrismaSql(orderBy, sessionsViewCols);
+2 -1
View File
@@ -1,5 +1,6 @@
import Redis, { RedisOptions } from "ioredis";
import { env } from "../../env";
import { logger } from "../logger";
export const createNewRedisInstance = (
additionalOptions: Partial<RedisOptions> = {},
@@ -26,7 +27,7 @@ const createRedisClient = () => {
try {
return createNewRedisInstance();
} catch (e) {
console.error(e, "Failed to connect to redis");
logger.error("Failed to connect to redis", e);
return null;
}
};
@@ -1,6 +1,7 @@
import { Prompt, PrismaClient } from "@prisma/client";
import { Redis } from "ioredis";
import { env } from "../../env";
import { logger } from "../logger";
export class PromptService {
private cacheEnabled: boolean;
@@ -11,7 +12,7 @@ export class PromptService {
private redis: Redis | null,
private metricIncrementer?: // used for otel metrics
(name: string, value?: number) => void,
cacheEnabled?: boolean // used for testing
cacheEnabled?: boolean, // used for testing
) {
this.cacheEnabled =
Boolean(redis) &&
@@ -25,7 +26,7 @@ export class PromptService {
const cachedPrompt = await this.getCachedPrompt(params);
this.incrementMetric(
cachedPrompt ? Metrics.PromptCacheHit : Metrics.PromptCacheMiss
cachedPrompt ? Metrics.PromptCacheHit : Metrics.PromptCacheMiss,
);
if (cachedPrompt) {
@@ -117,7 +118,7 @@ export class PromptService {
}
public async lockCache(
params: Pick<PromptParams, "projectId" | "promptName">
params: Pick<PromptParams, "projectId" | "promptName">,
): Promise<void> {
if (!this.cacheEnabled) return;
@@ -133,7 +134,7 @@ export class PromptService {
}
public async unlockCache(
params: Pick<PromptParams, "projectId" | "promptName">
params: Pick<PromptParams, "projectId" | "promptName">,
): Promise<void> {
if (!this.cacheEnabled) return;
@@ -149,7 +150,7 @@ export class PromptService {
}
private async isCacheLocked(
params: Pick<PromptParams, "projectId" | "promptName">
params: Pick<PromptParams, "projectId" | "promptName">,
): Promise<boolean> {
const lockKey = this.getLockKey(params);
@@ -163,14 +164,14 @@ export class PromptService {
}
private getLockKey(
params: Pick<PromptParams, "projectId" | "promptName">
params: Pick<PromptParams, "projectId" | "promptName">,
): string {
// Important to *pre*fix LOCK as otherwise it would be deleted by deleteKeysByPrefix
return `LOCK:${this.getCacheKeyPrefix(params)}`;
}
public async invalidateCache(
params: Pick<PromptParams, "projectId" | "promptName">
params: Pick<PromptParams, "projectId" | "promptName">,
): Promise<void> {
if (!this.cacheEnabled) return;
@@ -187,7 +188,7 @@ export class PromptService {
await this.redis?.del([...(keys ?? []), keyIndexKey]);
this.logInfo(
`Cache invalidated for prefix ${cacheKeyPrefix} in ${Date.now() - startTime}ms`
`Cache invalidated for prefix ${cacheKeyPrefix} in ${Date.now() - startTime}ms`,
);
} catch (e) {
this.logError("Error deleting keys for prefix", cacheKeyPrefix, e);
@@ -203,23 +204,23 @@ export class PromptService {
}
private getCacheKeyPrefix(
params: Pick<PromptParams, "projectId" | "promptName">
params: Pick<PromptParams, "projectId" | "promptName">,
): string {
return `prompt:${params.projectId}:${params.promptName}`;
}
private getKeyIndexKey(
params: Pick<PromptParams, "projectId" | "promptName">
params: Pick<PromptParams, "projectId" | "promptName">,
): string {
return `prompt_key_index:${params.projectId}:${params.promptName}`;
}
private logError(message: string, ...args: any[]) {
console.error(`[PromptService] ${message}`, ...args);
logger.error(`[PromptService] ${message}`, ...args);
}
private logInfo(message: string, ...args: any[]) {
console.log(`[PromptService] ${message}`, ...args);
logger.info(`[PromptService] ${message}`, ...args);
}
private incrementMetric(name: Metrics, value: number = 1) {
@@ -2,6 +2,7 @@ import type { Readable } from "stream";
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { logger } from "../logger";
type UploadFile = {
fileName: string;
@@ -56,15 +57,14 @@ export class S3StorageService {
return { signedUrl };
} catch (err) {
console.error(err);
logger.error(err);
throw new Error("Failed to upload to S3 or generate signed URL");
}
}
private async getSignedUrl(
fileName: string,
ttlSeconds: number
ttlSeconds: number,
): Promise<string> {
try {
return await getSignedUrl(
@@ -74,7 +74,7 @@ export class S3StorageService {
Key: fileName,
ResponseContentDisposition: `attachment; filename="${fileName}"`,
}),
{ expiresIn: ttlSeconds }
{ expiresIn: ttlSeconds },
);
} catch (err) {
throw Error("Failed to generate signed URL");
@@ -3,6 +3,7 @@ import { parseConnectionUrl } from "nodemailer/lib/shared/index.js";
import { render } from "@react-email/render";
import { BatchExportSuccessEmailTemplate } from "./BatchExportSuccessEmailTemplate";
import { logger } from "../../../logger";
type SendBatchExportSuccessParams = {
env: Partial<
@@ -24,8 +25,7 @@ export const sendBatchExportSuccessEmail = async ({
expiresInHours,
}: SendBatchExportSuccessParams) => {
if (!env.EMAIL_FROM_ADDRESS || !env.SMTP_CONNECTION_URL) {
console.error("Missing environment variables for sending email.");
logger.error("Missing environment variables for sending email.");
return;
}
@@ -38,7 +38,7 @@ export const sendBatchExportSuccessEmail = async ({
userName,
batchExportName,
expiresInHours,
})
}),
);
await mailer.sendMail({
@@ -51,6 +51,6 @@ export const sendBatchExportSuccessEmail = async ({
html: htmlTemplate,
});
} catch (error) {
console.error(error);
logger.error(error);
}
};
@@ -3,6 +3,7 @@ import { parseConnectionUrl } from "nodemailer/lib/shared/index.js";
import { render } from "@react-email/render";
import MembershipInvitationTemplate from "./MembershipInvitationEmailTemplate";
import { logger } from "../../../logger";
const langfuseUrls = {
US: "https://us.cloud.langfuse.com",
@@ -34,8 +35,8 @@ export const sendMembershipInvitationEmail = async ({
orgName,
}: SendMembershipInvitationParams) => {
if (!env.EMAIL_FROM_ADDRESS || !env.SMTP_CONNECTION_URL) {
console.error(
"Missing environment variables for sending membership invitation email."
logger.error(
"Missing environment variables for sending membership invitation email.",
);
return;
}
@@ -49,8 +50,8 @@ export const sendMembershipInvitationEmail = async ({
const authUrl = getAuthURL();
if (!authUrl) {
console.error(
"Missing NEXTAUTH_URL or NEXT_PUBLIC_LANGFUSE_CLOUD_REGION environment variable."
logger.error(
"Missing NEXTAUTH_URL or NEXT_PUBLIC_LANGFUSE_CLOUD_REGION environment variable.",
);
return;
}
@@ -67,7 +68,7 @@ export const sendMembershipInvitationEmail = async ({
inviteLink: authUrl,
emailFromAddress: env.EMAIL_FROM_ADDRESS,
langfuseCloudRegion: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION,
})
}),
);
await mailer.sendMail({
@@ -77,6 +78,6 @@ export const sendMembershipInvitationEmail = async ({
html: htmlTemplate,
});
} catch (error) {
console.error(error);
logger.error(error);
}
};
+391 -491
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.77.0",
"version": "2.78.0",
"private": true,
"license": "MIT",
"engines": {
@@ -46,6 +46,7 @@
"@opentelemetry/instrumentation": "^0.52.1",
"@opentelemetry/instrumentation-http": "^0.52.1",
"@opentelemetry/instrumentation-ioredis": "^0.42.0",
"@opentelemetry/instrumentation-winston": "^0.40.0",
"@opentelemetry/instrumentation-undici": "^0.4.0",
"@opentelemetry/resources": "^1.25.1",
"@opentelemetry/sdk-metrics": "1.23.0",
@@ -156,8 +157,8 @@
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.7",
"@types/node": "20.10.5",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"@types/react": "~18.2.79",
"@types/react-dom": "~18.2.25",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^6.21.0",
@@ -1,4 +1,4 @@
import { orderByToPrismaSql } from "@langfuse/shared";
import { orderByToPrismaSql } from "@langfuse/shared/src/server";
import { tracesTableCols } from "@langfuse/shared";
// The test for the orderByToPrisma function
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.77.0";
export const VERSION = "v2.78.0";
+4 -1
View File
@@ -8,9 +8,11 @@ import dd from "dd-trace";
import opentelemetry from "@opentelemetry/api";
// import { BullMQInstrumentation } from "@appsignal/opentelemetry-instrumentation-bullmq";
import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
import { logger } from "@langfuse/shared/src/server";
import { WinstonInstrumentation } from "@opentelemetry/instrumentation-winston";
if (!process.env.VERCEL && process.env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION) {
console.log("Initializing otel tracing");
logger.info("Initializing otel tracing");
const contextManager = new AsyncHooksContextManager().enable();
opentelemetry.context.setGlobalContextManager(contextManager);
@@ -51,6 +53,7 @@ if (!process.env.VERCEL && process.env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION) {
new IORedisInstrumentation(),
new HttpInstrumentation(),
new PrismaInstrumentation(),
new WinstonInstrumentation({ disableLogSending: true }),
getNodeAutoInstrumentations(),
new UndiciInstrumentation(),
// new BullMQInstrumentation(),
+18 -18
View File
@@ -171,15 +171,15 @@ export const env = createEnv({
SENTRY_AUTH_TOKEN: z.string().optional(),
SENTRY_CSP_REPORT_URI: z.string().optional(),
LANGFUSE_RATE_LIMITS_ENABLED: z.enum(["true", "false"]).default("true"),
LANGFUSE_PROVISION_ORG_ID: z.string().optional(),
LANGFUSE_PROVISION_ORG_NAME: z.string().optional(),
LANGFUSE_PROVISION_PROJECT_ID: z.string().optional(),
LANGFUSE_PROVISION_PROJECT_NAME: z.string().optional(),
LANGFUSE_PROVISION_PROJECT_PUBLIC_KEY: z.string().optional(),
LANGFUSE_PROVISION_PROJECT_SECRET_KEY: z.string().optional(),
LANGFUSE_PROVISION_USER_EMAIL: z.string().email().optional(),
LANGFUSE_PROVISION_USER_NAME: z.string().optional(),
LANGFUSE_PROVISION_USER_PASSWORD: z.string().optional(),
LANGFUSE_INIT_ORG_ID: z.string().optional(),
LANGFUSE_INIT_ORG_NAME: z.string().optional(),
LANGFUSE_INIT_PROJECT_ID: z.string().optional(),
LANGFUSE_INIT_PROJECT_NAME: z.string().optional(),
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: z.string().optional(),
LANGFUSE_INIT_PROJECT_SECRET_KEY: z.string().optional(),
LANGFUSE_INIT_USER_EMAIL: z.string().email().optional(),
LANGFUSE_INIT_USER_NAME: z.string().optional(),
LANGFUSE_INIT_USER_PASSWORD: z.string().optional(),
},
/**
@@ -333,15 +333,15 @@ export const env = createEnv({
SENTRY_CSP_REPORT_URI: process.env.SENTRY_CSP_REPORT_URI,
LANGFUSE_RATE_LIMITS_ENABLED: process.env.LANGFUSE_RATE_LIMITS_ENABLED,
// provisioning
LANGFUSE_PROVISION_ORG_ID: process.env.LANGFUSE_PROVISION_ORG_ID,
LANGFUSE_PROVISION_ORG_NAME: process.env.LANGFUSE_PROVISION_ORG_NAME,
LANGFUSE_PROVISION_PROJECT_ID: process.env.LANGFUSE_PROVISION_PROJECT_ID,
LANGFUSE_PROVISION_PROJECT_NAME: process.env.LANGFUSE_PROVISION_PROJECT_NAME,
LANGFUSE_PROVISION_PROJECT_PUBLIC_KEY: process.env.LANGFUSE_PROVISION_PROJECT_PUBLIC_KEY,
LANGFUSE_PROVISION_PROJECT_SECRET_KEY: process.env.LANGFUSE_PROVISION_PROJECT_SECRET_KEY,
LANGFUSE_PROVISION_USER_EMAIL: process.env.LANGFUSE_PROVISION_USER_EMAIL,
LANGFUSE_PROVISION_USER_NAME: process.env.LANGFUSE_PROVISION_USER_NAME,
LANGFUSE_PROVISION_USER_PASSWORD: process.env.LANGFUSE_PROVISION_USER_PASSWORD,
LANGFUSE_INIT_ORG_ID: process.env.LANGFUSE_INIT_ORG_ID,
LANGFUSE_INIT_ORG_NAME: process.env.LANGFUSE_INIT_ORG_NAME,
LANGFUSE_INIT_PROJECT_ID: process.env.LANGFUSE_INIT_PROJECT_ID,
LANGFUSE_INIT_PROJECT_NAME: process.env.LANGFUSE_INIT_PROJECT_NAME,
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: process.env.LANGFUSE_INIT_PROJECT_PUBLIC_KEY,
LANGFUSE_INIT_PROJECT_SECRET_KEY: process.env.LANGFUSE_INIT_PROJECT_SECRET_KEY,
LANGFUSE_INIT_USER_EMAIL: process.env.LANGFUSE_INIT_USER_EMAIL,
LANGFUSE_INIT_USER_NAME: process.env.LANGFUSE_INIT_USER_NAME,
LANGFUSE_INIT_USER_PASSWORD: process.env.LANGFUSE_INIT_USER_PASSWORD,
},
// Skip validation in Docker builds
// DOCKER_BUILD is set in Dockerfile
@@ -3,6 +3,7 @@ import { createUserEmailPassword } from "@/src/features/auth-credentials/lib/cre
import { signupSchema } from "@/src/features/auth/lib/signupSchema";
import { getSsoAuthProviderIdForDomain } from "@/src/ee/features/multi-tenant-sso/utils";
import type { NextApiRequest, NextApiResponse } from "next";
import { logger } from "@langfuse/shared/src/server";
/*
* Sign-up endpoint (email/password users), creates user in database.
@@ -32,7 +33,7 @@ export async function signupApiHandler(
// parse and type check the request body with zod
const validBody = signupSchema.safeParse(req.body);
if (!validBody.success) {
console.log("Signup: Invalid body", validBody.error);
logger.warn("Signup: Invalid body", validBody.error);
res.status(422).json({ message: validBody.error });
return;
}
@@ -69,11 +70,12 @@ export async function signupApiHandler(
);
} catch (error) {
if (error instanceof Error) {
console.log(
logger.warn(
"Signup: Error creating user",
error.message,
body.email.toLowerCase(),
body.name,
error,
);
res.status(422).json({ message: error.message });
}
@@ -1,5 +1,6 @@
import { env } from "@/src/env.mjs";
import { prisma, Role } from "@langfuse/shared/src/db";
import { logger } from "@langfuse/shared/src/server";
export async function createProjectMembershipsOnSignup(user: {
id: string;
@@ -81,7 +82,7 @@ export async function createProjectMembershipsOnSignup(user: {
// Invites do not work for users without emails (some future SSO users)
if (user.email) await processMembershipInvitations(user.email, user.id);
} catch (e) {
console.error("Error assigning project access to new user", e);
logger.error("Error assigning project access to new user", e);
}
}
@@ -1,6 +1,7 @@
import { runFeedbackCorsMiddleware } from "@/src/features/feedback/server/corsMiddleware";
import { sendToSlack } from "@/src/features/slack/server/slack-webhook";
import { type NextApiRequest, type NextApiResponse } from "next";
import { logger } from "@langfuse/shared/src/server";
// Collects feedack from users that do not use the cloud version of the app
export default async function feedbackApiHandler(
@@ -14,11 +15,11 @@ export default async function feedbackApiHandler(
if (slackResponse.status === 200) {
res.status(200).json({ status: "OK" });
} else {
console.error(slackResponse);
logger.error(slackResponse);
res.status(400).json({ status: "Error" });
}
} catch (error) {
console.error(error);
logger.error(error);
res.status(500).json({ status: "Error" });
}
}
@@ -14,6 +14,7 @@ import {
supportedModels,
} from "@langfuse/shared";
import { encrypt } from "@langfuse/shared/encryption";
import { logger } from "@langfuse/shared/src/server";
export function getDisplaySecretKey(secretKey: string) {
return "..." + secretKey.slice(-4);
@@ -50,7 +51,7 @@ export const llmApiKeyRouter = createTRPCRouter({
action: "create",
});
} catch (e) {
console.log(e);
logger.error(e);
throw e;
}
}),
@@ -160,7 +161,7 @@ export const llmApiKeyRouter = createTRPCRouter({
return { success: true };
} catch (err) {
console.log(err);
logger.error(err);
return {
success: false,
@@ -1,10 +1,8 @@
import { type GetPromptsMetaType } from "@/src/features/prompts/server/utils/validation";
import { promptsTableCols } from "@/src/server/api/definitions/promptsTable";
import {
tableColumnsToSqlFilterAndPrefix,
type FilterState,
} from "@langfuse/shared";
import { type FilterState } from "@langfuse/shared";
import { prisma } from "@langfuse/shared/src/db";
import { tableColumnsToSqlFilterAndPrefix } from "@langfuse/shared/src/server";
export type GetPromptsMetaParams = GetPromptsMetaType & { projectId: string };
@@ -10,16 +10,18 @@ import {
import { type Prompt, Prisma } from "@langfuse/shared/src/db";
import { createPrompt } from "../actions/createPrompt";
import { observationsTableCols, orderByToPrismaSql } from "@langfuse/shared";
import { observationsTableCols } from "@langfuse/shared";
import { promptsTableCols } from "@/src/server/api/definitions/promptsTable";
import { optionalPaginationZod, paginationZod } from "@langfuse/shared";
import {
orderBy,
singleFilter,
tableColumnsToSqlFilterAndPrefix,
} from "@langfuse/shared";
import { orderBy, singleFilter } from "@langfuse/shared";
import { LATEST_PROMPT_LABEL } from "@/src/features/prompts/constants";
import { PromptService, redis } from "@langfuse/shared/src/server";
import {
orderByToPrismaSql,
PromptService,
redis,
logger,
tableColumnsToSqlFilterAndPrefix,
} from "@langfuse/shared/src/server";
import { aggregateScores } from "@/src/features/scores/lib/aggregateScores";
import { type ScoreSimplified } from "@/src/features/scores/lib/types";
@@ -192,7 +194,7 @@ export const promptRouter = createTRPCRouter({
return prompt;
} catch (e) {
console.log(e);
logger.error(e);
throw e;
}
}),
@@ -296,7 +298,7 @@ export const promptRouter = createTRPCRouter({
// Unlock cache
await promptService.unlockCache({ projectId, promptName });
} catch (e) {
console.log(e);
logger.error(e);
throw e;
}
}),
@@ -384,7 +386,7 @@ export const promptRouter = createTRPCRouter({
// Unlock cache
await promptService.unlockCache({ projectId, promptName });
} catch (e) {
console.log(e);
logger.error(e);
throw e;
}
}),
@@ -478,7 +480,7 @@ export const promptRouter = createTRPCRouter({
// Unlock cache
await promptService.unlockCache({ projectId, promptName });
} catch (e) {
console.log(e);
logger.error(e);
throw e;
}
}),
@@ -546,7 +548,7 @@ export const promptRouter = createTRPCRouter({
// Unlock cache
await promptService.unlockCache({ projectId, promptName });
} catch (error) {
console.error(error);
logger.error(error);
}
}),
allVersions: protectedProjectProcedure
@@ -11,6 +11,7 @@ import {
import {
recordIncrement,
type ApiAccessScope,
logger,
} from "@langfuse/shared/src/server";
import { type NextApiResponse } from "next";
@@ -41,7 +42,7 @@ export class RateLimitService {
}
if (!this.redis) {
console.log("Rate limiting not available without Redis");
logger.warn("Rate limiting not available without Redis");
return new RateLimitHelper(undefined);
}
@@ -100,7 +101,7 @@ export class RateLimitService {
};
} else {
// Some other error occurred, rethrow it
console.log("Internal Rate limit error", err);
logger.error("Internal Rate limit error", err);
throw err;
}
}
@@ -134,9 +135,7 @@ export class RateLimitHelper {
sendRestResponseIfLimited(nextResponse: NextApiResponse) {
if (!this.res || !this.isRateLimited()) {
console.error(
"Trying to send rate limit response without being limited.",
);
logger.error("Trying to send rate limit response without being limited.");
throw new Error(
"Trying to send rate limit response without being limited.",
);
+15 -17
View File
@@ -7,6 +7,7 @@ import {
type AuthHeaderVerificationResult,
CachedApiKey,
OrgEnrichedApiKey,
logger,
} from "@langfuse/shared/src/server";
import {
type PrismaClient,
@@ -39,12 +40,12 @@ export class ApiAuthService {
Boolean(hash),
);
if (filteredHashKeys.length === 0) {
console.log("No valid keys to invalidate");
logger.info("No valid keys to invalidate");
return;
}
if (this.redis) {
console.log(`Invalidating API keys in redis for ${identifier}`);
logger.info(`Invalidating API keys in redis for ${identifier}`);
await this.redis.del(
filteredHashKeys
.filter((hash): hash is string => Boolean(hash))
@@ -103,7 +104,7 @@ export class ApiAuthService {
authHeader: string | undefined,
): Promise<AuthHeaderVerificationResult> {
if (!authHeader) {
console.error("No authorization header");
logger.error("No authorization header");
return {
validKey: false,
error: "No authorization header",
@@ -132,9 +133,9 @@ export class ApiAuthService {
});
if (!slowKey) {
console.error("No key found for public key", publicKey);
logger.error("No key found for public key", publicKey);
if (this.redis) {
console.log(
logger.info(
`No key found, storing ${API_KEY_NON_EXISTENT} in redis`,
);
await this.addApiKeyToRedis(
@@ -151,7 +152,7 @@ export class ApiAuthService {
);
if (!isValid) {
console.log("Old key is invalid", publicKey);
logger.info("Old key is invalid", publicKey);
throw new Error("Invalid credentials");
}
@@ -170,7 +171,7 @@ export class ApiAuthService {
}
if (!finalApiKey) {
console.log("No project id found for key", publicKey);
logger.info("No project id found for key", publicKey);
throw new Error("Invalid credentials");
}
@@ -179,7 +180,7 @@ export class ApiAuthService {
const plan = finalApiKey.plan;
if (!isPlan(plan)) {
console.error("Invalid plan type for key", finalApiKey.plan);
logger.error("Invalid plan type for key", finalApiKey.plan);
throw new Error("Invalid credentials");
}
@@ -218,7 +219,7 @@ export class ApiAuthService {
};
}
} catch (error: unknown) {
console.error(
logger.error(
`Error verifying auth header: ${error instanceof Error ? error.message : null}`,
error,
);
@@ -258,7 +259,7 @@ export class ApiAuthService {
include: { project: { include: { organization: true } } },
});
if (!dbKey) {
console.log("No api key found for public key:", publicKey);
logger.info("No api key found for public key:", publicKey);
throw new Error("Invalid public key");
}
return dbKey;
@@ -316,7 +317,7 @@ export class ApiAuthService {
env.LANGFUSE_CACHE_API_KEY_TTL_SECONDS, // redis API is in seconds
);
} catch (error: unknown) {
console.error("Error adding key to redis", error);
logger.error("Error adding key to redis", error);
}
}
@@ -343,14 +344,11 @@ export class ApiAuthService {
}
if (!parsedApiKey.success) {
console.error(
"Failed to parse API key from Redis:",
parsedApiKey.error,
);
logger.error("Failed to parse API key from Redis:", parsedApiKey.error);
}
return null;
} catch (error: unknown) {
console.error("Error fetching key from redis", error);
logger.error("Error fetching key from redis", error);
return null;
}
}
@@ -393,7 +391,7 @@ export const convertToRedisRepresentation = (
});
if (!orgId) {
console.error("No organization found for key");
logger.error("No organization found for key");
throw new Error("Invalid credentials");
}
@@ -1,5 +1,5 @@
import { prisma } from "@langfuse/shared/src/db";
import { type ApiAccessScope } from "@langfuse/shared/src/server";
import { type ApiAccessScope, logger } from "@langfuse/shared/src/server";
type Resource = {
type: "project" | "trace" | "observation" | "score";
@@ -28,7 +28,7 @@ async function isResourceInProject(resource: Resource, projectId: string) {
case "project":
const projectCheck = resource.id === projectId;
if (!projectCheck)
console.log("project check", projectCheck, resource.id, projectId);
logger.warn("project check", projectCheck, resource.id, projectId);
return projectCheck;
case "trace":
@@ -37,7 +37,7 @@ async function isResourceInProject(resource: Resource, projectId: string) {
where: { id: resource.id, projectId },
})) === 1;
if (!traceCheck)
console.log("trace check", traceCheck, resource.id, projectId);
logger.warn("trace check", traceCheck, resource.id, projectId);
return traceCheck;
case "observation":
@@ -46,7 +46,7 @@ async function isResourceInProject(resource: Resource, projectId: string) {
where: { id: resource.id, projectId },
})) === 1;
if (!observationCheck)
console.log(
logger.warn(
"observation check",
observationCheck,
resource.id,
@@ -63,7 +63,7 @@ async function isResourceInProject(resource: Resource, projectId: string) {
},
})) === 1;
if (!scoreCheck)
console.log("score check", scoreCheck, resource.id, projectId);
logger.warn("score check", scoreCheck, resource.id, projectId);
return scoreCheck;
default:
return false;
@@ -6,6 +6,7 @@ import {
redis,
type AuthHeaderValidVerificationResult,
traceException,
logger,
} from "@langfuse/shared/src/server";
import { type RateLimitResource } from "@langfuse/shared";
import { RateLimitService } from "@/src/features/public-api/server/RateLimitService";
@@ -64,15 +65,8 @@ export const createAuthedAPIRoute = <
return rateLimitResponse.sendRestResponseIfLimited(res);
}
console.log(
"Request to route ",
routeConfig.name,
"projectId ",
auth.scope.projectId,
"with query ",
req.query,
"and body ",
req.body,
logger.info(
`Request to route ${routeConfig.name} projectId ${auth.scope.projectId} with query ${req.query} and body ${req.body}`,
);
const query = routeConfig.querySchema
@@ -93,7 +87,7 @@ export const createAuthedAPIRoute = <
if (routeConfig.responseSchema) {
const parsingResult = routeConfig.responseSchema.safeParse(response);
if (!parsingResult.success) {
console.error("Response validation failed:", parsingResult.error);
logger.error("Response validation failed:", parsingResult.error);
traceException(parsingResult.error);
}
}
@@ -3,7 +3,7 @@ import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { type NextApiRequest, type NextApiResponse } from "next";
import { type ZodError } from "zod";
import { BaseError, MethodNotAllowedError } from "@langfuse/shared";
import { traceException } from "@langfuse/shared/src/server";
import { logger, traceException } from "@langfuse/shared/src/server";
const httpMethods = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const;
export type HttpMethod = (typeof httpMethods)[number];
@@ -39,7 +39,7 @@ export function withMiddlewares(handlers: Handlers) {
return await finalHandlers[method](req, res);
} catch (error) {
console.error(error);
logger.error(error);
if (error instanceof BaseError) {
if (error.httpCode >= 500 && error.httpCode < 600) {
+6 -5
View File
@@ -2,6 +2,7 @@ import { VERSION } from "@/src/constants";
import { ServerPosthog } from "@/src/features/posthog-analytics/ServerPosthog";
import { Prisma, prisma } from "@langfuse/shared/src/db";
import { v4 as uuidv4 } from "uuid";
import { logger } from "@langfuse/shared/src/server";
// Interval between jobs in milliseconds
const JOB_INTERVAL_MINUTES = Prisma.raw("60");
@@ -45,7 +46,7 @@ export async function telemetry() {
}
} catch (error) {
// Catch all errors to be sure telemetry does not break the application
console.error("Telemetry, unexpected error:", error);
logger.error("Telemetry, unexpected error:", error);
}
}
@@ -85,7 +86,7 @@ async function jobScheduler(): Promise<
) AS status;`;
// Return if job should not run
if (checkNoLock.length !== 1) {
console.error("Telemetry failed to check if job should run");
logger.error("Telemetry failed to check if job should run");
return { shouldRunJob: false };
}
if (!checkNoLock[0]!.status) return { shouldRunJob: false };
@@ -119,7 +120,7 @@ async function jobScheduler(): Promise<
// Other job was created in the meantime
if (createJobLocked.length !== 1) {
console.error("Telemetry job is locked");
logger.error("Telemetry job is locked");
return { shouldRunJob: false };
}
@@ -127,7 +128,7 @@ async function jobScheduler(): Promise<
// should not happen
if (!jobStartedAt) {
console.error("Telemetry failed to create job_started_at");
logger.error("Telemetry failed to create job_started_at");
return { shouldRunJob: false };
}
@@ -263,6 +264,6 @@ async function posthogTelemetry({
await posthog.shutdownAsync();
} catch (error) {
console.error(error);
logger.error(error);
}
}
@@ -4,59 +4,59 @@ import { prisma } from "@langfuse/shared/src/db";
import { createAndAddApiKeysToDb } from "@langfuse/shared/src/server/auth/apiKeys";
// Create Organization
if (env.LANGFUSE_PROVISION_ORG_ID) {
if (env.LANGFUSE_INIT_ORG_ID) {
const org = await prisma.organization.upsert({
where: { id: env.LANGFUSE_PROVISION_ORG_ID },
where: { id: env.LANGFUSE_INIT_ORG_ID },
update: {},
create: {
id: env.LANGFUSE_PROVISION_ORG_ID,
name: env.LANGFUSE_PROVISION_ORG_NAME ?? "Provisioned Org",
id: env.LANGFUSE_INIT_ORG_ID,
name: env.LANGFUSE_INIT_ORG_NAME ?? "Provisioned Org",
},
});
// Create Project: Org -> Project
if (env.LANGFUSE_PROVISION_PROJECT_ID) {
if (env.LANGFUSE_INIT_PROJECT_ID) {
await prisma.project.upsert({
where: { id: env.LANGFUSE_PROVISION_PROJECT_ID },
where: { id: env.LANGFUSE_INIT_PROJECT_ID },
update: {},
create: {
id: env.LANGFUSE_PROVISION_PROJECT_ID,
name: env.LANGFUSE_PROVISION_PROJECT_NAME ?? "Provisioned Project",
id: env.LANGFUSE_INIT_PROJECT_ID,
name: env.LANGFUSE_INIT_PROJECT_NAME ?? "Provisioned Project",
orgId: org.id,
},
});
// Add API Keys: Project -> API Key
if (
env.LANGFUSE_PROVISION_PROJECT_SECRET_KEY &&
env.LANGFUSE_PROVISION_PROJECT_PUBLIC_KEY
env.LANGFUSE_INIT_PROJECT_SECRET_KEY &&
env.LANGFUSE_INIT_PROJECT_PUBLIC_KEY
) {
const existingApiKey = await prisma.apiKey.findUnique({
where: { publicKey: env.LANGFUSE_PROVISION_PROJECT_PUBLIC_KEY },
where: { publicKey: env.LANGFUSE_INIT_PROJECT_PUBLIC_KEY },
});
// Delete key if project changed
if (
existingApiKey &&
existingApiKey.projectId !== env.LANGFUSE_PROVISION_PROJECT_ID
existingApiKey.projectId !== env.LANGFUSE_INIT_PROJECT_ID
) {
await prisma.apiKey.delete({
where: { publicKey: env.LANGFUSE_PROVISION_PROJECT_PUBLIC_KEY },
where: { publicKey: env.LANGFUSE_INIT_PROJECT_PUBLIC_KEY },
});
}
// Create new key if it doesn't exist or project changed
if (
!existingApiKey ||
existingApiKey.projectId !== env.LANGFUSE_PROVISION_PROJECT_ID
existingApiKey.projectId !== env.LANGFUSE_INIT_PROJECT_ID
) {
await createAndAddApiKeysToDb({
prisma,
projectId: env.LANGFUSE_PROVISION_PROJECT_ID,
projectId: env.LANGFUSE_INIT_PROJECT_ID,
note: "Provisioned API Key",
predefinedKeys: {
secretKey: env.LANGFUSE_PROVISION_PROJECT_SECRET_KEY,
publicKey: env.LANGFUSE_PROVISION_PROJECT_PUBLIC_KEY,
secretKey: env.LANGFUSE_INIT_PROJECT_SECRET_KEY,
publicKey: env.LANGFUSE_INIT_PROJECT_PUBLIC_KEY,
},
});
}
@@ -64,12 +64,9 @@ if (env.LANGFUSE_PROVISION_ORG_ID) {
}
// Create User: Org -> User
if (
env.LANGFUSE_PROVISION_USER_EMAIL &&
env.LANGFUSE_PROVISION_USER_PASSWORD
) {
if (env.LANGFUSE_INIT_USER_EMAIL && env.LANGFUSE_INIT_USER_PASSWORD) {
const existingUser = await prisma.user.findUnique({
where: { email: env.LANGFUSE_PROVISION_USER_EMAIL },
where: { email: env.LANGFUSE_INIT_USER_EMAIL },
});
let userId = existingUser?.id;
@@ -77,9 +74,9 @@ if (env.LANGFUSE_PROVISION_ORG_ID) {
// Create user if it doesn't exist yet
if (!userId) {
userId = await createUserEmailPassword(
env.LANGFUSE_PROVISION_USER_EMAIL,
env.LANGFUSE_PROVISION_USER_PASSWORD,
env.LANGFUSE_PROVISION_USER_NAME ?? "Provisioned User",
env.LANGFUSE_INIT_USER_EMAIL,
env.LANGFUSE_INIT_USER_PASSWORD,
env.LANGFUSE_INIT_USER_NAME ?? "Provisioned User",
);
}
+1 -1
View File
@@ -3,6 +3,6 @@
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./datadog.server.config");
await import("./provisioning");
await import("./initialize");
}
}
+3 -2
View File
@@ -2,6 +2,7 @@ import { env } from "@/src/env.mjs";
import { ServerPosthog } from "@/src/features/posthog-analytics/ServerPosthog";
import { prisma } from "@langfuse/shared/src/db";
import { type NextApiRequest, type NextApiResponse } from "next";
import { logger } from "@langfuse/shared/src/server";
export default async function handler(
req: NextApiRequest,
@@ -59,7 +60,7 @@ export default async function handler(
await posthog.shutdownAsync();
console.log(
logger.info(
"Updated ingestion_metrics in PostHog from startTimeframe:",
startTimeframe?.toISOString(),
"to endTimeframe:",
@@ -77,7 +78,7 @@ export default async function handler(
return res.status(200).json({ message: "OK" });
} catch (error) {
console.error(error);
logger.error(error);
return res.status(500).json({ message: "Internal server error" });
}
}
+3 -13
View File
@@ -1,9 +1,8 @@
import { VERSION } from "@/src/constants";
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { telemetry } from "@/src/features/telemetry";
import { isSigtermReceived } from "@/src/utils/shutdown";
import { prisma } from "@langfuse/shared/src/db";
import { traceException } from "@langfuse/shared/src/server";
import { logger, traceException } from "@langfuse/shared/src/server";
import { type NextApiRequest, type NextApiResponse } from "next";
export default async function handler(
@@ -16,15 +15,6 @@ export default async function handler(
const failIfNoRecentEvents = req.query.failIfNoRecentEvents === "true";
try {
if (isSigtermReceived()) {
console.log(
"Health check failed: SIGTERM / SIGINT received, shutting down.",
);
return res.status(500).json({
status: "SIGTERM / SIGINT received, shutting down",
version: VERSION.replace("v", ""),
});
}
await prisma.$queryRaw`SELECT 1;`;
if (failIfNoRecentEvents) {
@@ -65,7 +55,7 @@ export default async function handler(
}
}
} catch (e) {
console.log("Health check failed: db not available", e);
logger.error("Health check failed: db not available", e);
traceException(e);
return res.status(503).json({
status: "Database not available",
@@ -74,7 +64,7 @@ export default async function handler(
}
} catch (e) {
traceException(e);
console.log("Health check failed: ", e);
logger.error("Health check failed: ", e);
return res.status(503).json({
status: "Health check failed",
version: VERSION.replace("v", ""),
+17 -9
View File
@@ -7,6 +7,7 @@ import {
ingestionEvent,
traceException,
redis,
logger,
type AuthHeaderValidVerificationResult,
type ingestionBatchEvent,
handleBatch,
@@ -35,6 +36,7 @@ import {
import {
sendToWorkerIfEnvironmentConfigured,
QueueJobs,
instrumentSync,
} from "@langfuse/shared/src/server";
import { randomUUID } from "crypto";
import { prisma } from "@langfuse/shared/src/db";
@@ -80,7 +82,10 @@ export default async function handler(
metadata: jsonSchema.nullish(),
});
const parsedSchema = batchType.safeParse(req.body);
const parsedSchema = instrumentSync(
{ name: "ingestion-zod-parse-unknown-batch-event" },
() => batchType.safeParse(req.body),
);
recordIncrement(
"ingestion_event",
@@ -105,7 +110,7 @@ export default async function handler(
: undefined;
if (!parsedSchema.success) {
console.log("Invalid request data", parsedSchema.error);
logger.info("Invalid request data", parsedSchema.error);
return res.status(400).json({
message: "Invalid request data",
errors: parsedSchema.error.issues.map((issue) => issue.message),
@@ -116,7 +121,10 @@ export default async function handler(
const batch: (z.infer<typeof ingestionEvent> | undefined)[] =
parsedSchema.data.batch.map((event) => {
const parsed = ingestionEvent.safeParse(event);
const parsed = instrumentSync(
{ name: "ingestion-zod-parse-individual-event" },
() => ingestionEvent.safeParse(event),
);
if (!parsed.success) {
validationErrors.push({
id:
@@ -169,7 +177,7 @@ export default async function handler(
},
);
} catch (e: unknown) {
console.warn(
logger.warn(
"Failed to add batch to queue, falling back to sync processing",
e,
);
@@ -190,7 +198,7 @@ export default async function handler(
);
}
} else {
console.error(
logger.error(
"Ingestion queue not initialized, falling back to sync processing",
);
}
@@ -212,7 +220,7 @@ export default async function handler(
);
} catch (error: unknown) {
if (!(error instanceof UnauthorizedError)) {
console.error("error_handling_ingestion_event", error);
logger.error("error_handling_ingestion_event", error);
traceException(error);
}
@@ -229,7 +237,7 @@ export default async function handler(
});
}
if (error instanceof z.ZodError) {
console.log(`Zod exception`, error.errors);
logger.info(`Zod exception`, error.errors);
return res.status(400).json({
message: "Invalid request data",
error: error.errors,
@@ -376,7 +384,7 @@ export const handleBatchResult = (
if (returnedErrors.length > 0) {
traceException(errors);
console.log("Error processing events", returnedErrors);
logger.info("Error processing events", returnedErrors);
}
results.forEach((result) => {
@@ -457,7 +465,7 @@ export const parseSingleTypedIngestionApiResponse = <T extends z.ZodTypeAny>(
const parsedObj = object.safeParse(results[0].result);
if (!parsedObj.success) {
console.error("Error parsing response", parsedObj.error);
logger.error("Error parsing response", parsedObj.error);
traceException(parsedObj.error);
}
// should not fail in prod but just log an exception, see above
+3 -3
View File
@@ -2,7 +2,7 @@ import { ApiAuthService } from "@/src/features/public-api/server/apiAuth";
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { prisma } from "@langfuse/shared/src/db";
import { isPrismaException } from "@/src/utils/exceptions";
import { redis } from "@langfuse/shared/src/server";
import { logger, redis } from "@langfuse/shared/src/server";
import { type NextApiRequest, type NextApiResponse } from "next";
import { RateLimitService } from "@/src/features/public-api/server/RateLimitService";
@@ -48,7 +48,7 @@ export default async function handler(
})),
});
} catch (error) {
console.error(error);
logger.error(error);
if (isPrismaException(error)) {
return res.status(500).json({
error: "Internal Server Error",
@@ -57,7 +57,7 @@ export default async function handler(
return res.status(500).json({ message: "Internal server error" });
}
} else {
console.error(
logger.error(
`Method not allowed for ${req.method} on /api/public/projects`,
);
return res.status(405).json({ message: "Method not allowed" });
+2 -1
View File
@@ -22,6 +22,7 @@ import {
redis,
recordIncrement,
traceException,
logger,
} from "@langfuse/shared/src/server";
import { PRODUCTION_LABEL } from "@/src/features/prompts/constants";
import { RateLimitService } from "@/src/features/public-api/server/RateLimitService";
@@ -111,7 +112,7 @@ export default async function handler(
throw new MethodNotAllowedError();
} catch (error: unknown) {
console.error(error);
logger.error(error);
traceException(error);
if (error instanceof BaseError) {
+37
View File
@@ -0,0 +1,37 @@
import { VERSION } from "@/src/constants";
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { telemetry } from "@/src/features/telemetry";
import { isSigtermReceived } from "@/src/utils/shutdown";
import { logger, traceException } from "@langfuse/shared/src/server";
import { type NextApiRequest, type NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
try {
await runMiddleware(req, res, cors);
await telemetry();
if (isSigtermReceived()) {
logger.info(
"Readiness check failed: SIGTERM / SIGINT received, shutting down.",
);
return res.status(500).json({
status: "SIGTERM / SIGINT received, shutting down",
version: VERSION.replace("v", ""),
});
}
} catch (e) {
traceException(e);
logger.warn("Readiness check failed: ", e);
return res.status(503).json({
status: "Readiness check failed",
version: VERSION.replace("v", ""),
});
}
return res.status(200).json({
status: "OK",
version: VERSION.replace("v", ""),
});
}
+6 -2
View File
@@ -10,11 +10,15 @@ import { createAuthedAPIRoute } from "@/src/features/public-api/server/createAut
import { Prisma } from "@langfuse/shared/src/db";
import { parseSingleTypedIngestionApiResponse } from "@/src/pages/api/public/ingestion";
import { type Trace } from "@langfuse/shared";
import { eventTypes, handleBatch } from "@langfuse/shared/src/server";
import {
eventTypes,
handleBatch,
orderByToPrismaSql,
} from "@langfuse/shared/src/server";
import { v4 } from "uuid";
import { telemetry } from "@/src/features/telemetry";
import { tracesTableCols, orderByToPrismaSql } from "@langfuse/shared";
import { tracesTableCols } from "@langfuse/shared";
import { tokenCount } from "@/src/features/ingest/usage";
export default withMiddlewares({
+3 -3
View File
@@ -10,7 +10,7 @@ import { prisma } from "@langfuse/shared/src/db";
import { ApiAuthService } from "@/src/features/public-api/server/apiAuth";
import { paginationZod } from "@langfuse/shared";
import { isPrismaException } from "@/src/utils/exceptions";
import { redis } from "@langfuse/shared/src/server";
import { logger, redis } from "@langfuse/shared/src/server";
import { RateLimitService } from "@/src/features/public-api/server/RateLimitService";
const GetUsersSchema = z.object({
@@ -131,11 +131,11 @@ export default async function handler(
},
});
} else {
console.error(req.method, req.body);
logger.error(`Invalid request method ${req.method}`, req.body);
return res.status(405).json({ message: "Method not allowed" });
}
} catch (error: unknown) {
console.error(error);
logger.error(error);
if (isPrismaException(error)) {
return res.status(500).json({
errors: ["Internal Server Error"],
+5 -2
View File
@@ -2,7 +2,7 @@ import { createNextApiHandler } from "@trpc/server/adapters/next";
import { createTRPCContext } from "@/src/server/api/trpc";
import { appRouter } from "@/src/server/api/root";
import { env } from "@/src/env.mjs";
import { traceException } from "@langfuse/shared/src/server";
import { logger, traceException } from "@langfuse/shared/src/server";
export const config = {
maxDuration: 240,
@@ -13,7 +13,10 @@ export default createNextApiHandler({
router: appRouter,
createContext: createTRPCContext,
onError: ({ path, error }) => {
console.error(`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`);
logger.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
error,
);
traceException(error);
},
responseMeta() {
@@ -1,15 +1,17 @@
import { aggregateScores } from "@/src/features/scores/lib/aggregateScores";
import {
datetimeFilterToPrismaSql,
filterAndValidateDbScoreList,
observationsTableCols,
orderByToPrismaSql,
tableColumnsToSqlFilterAndPrefix,
} from "@langfuse/shared";
import { type ObservationView, Prisma, prisma } from "@langfuse/shared/src/db";
import { type GetAllGenerationsInput } from "../getAllQueries";
import { traceException } from "@langfuse/shared/src/server";
import {
datetimeFilterToPrismaSql,
orderByToPrismaSql,
tableColumnsToSqlFilterAndPrefix,
traceException,
} from "@langfuse/shared/src/server";
type AdditionalObservationFields = {
traceName: string | null;
@@ -1,13 +1,12 @@
import { z } from "zod";
import { timeFilter, type ObservationOptions } from "@langfuse/shared";
import { protectedProjectProcedure } from "@/src/server/api/trpc";
import { Prisma } from "@langfuse/shared/src/db";
import {
datetimeFilterToPrisma,
datetimeFilterToPrismaSql,
timeFilter,
type ObservationOptions,
} from "@langfuse/shared";
import { protectedProjectProcedure } from "@/src/server/api/trpc";
import { Prisma } from "@langfuse/shared/src/db";
} from "@langfuse/shared/src/server";
export const filterOptionsQuery = protectedProjectProcedure
.input(
+6 -4
View File
@@ -17,18 +17,20 @@ import {
} from "@/src/server/api/trpc";
import {
CreateAnnotationScoreData,
datetimeFilterToPrismaSql,
datetimeFilterToPrisma,
orderBy,
orderByToPrismaSql,
paginationZod,
singleFilter,
tableColumnsToSqlFilterAndPrefix,
timeFilter,
UpdateAnnotationScoreData,
validateDbScore,
} from "@langfuse/shared";
import { Prisma, type Score } from "@langfuse/shared/src/db";
import {
datetimeFilterToPrisma,
datetimeFilterToPrismaSql,
orderByToPrismaSql,
tableColumnsToSqlFilterAndPrefix,
} from "@langfuse/shared/src/server";
const ScoreFilterOptions = z.object({
projectId: z.string(), // Required for protectedProjectProcedure
+5 -3
View File
@@ -9,19 +9,21 @@ import {
} from "@/src/server/api/trpc";
import {
filterAndValidateDbScoreList,
createSessionsAllQuery,
orderBy,
paginationZod,
type SessionOptions,
singleFilter,
timeFilter,
datetimeFilterToPrismaSql,
} from "@langfuse/shared";
import { Prisma } from "@langfuse/shared/src/db";
import { TRPCError } from "@trpc/server";
import type Decimal from "decimal.js";
import { traceException } from "@langfuse/shared/src/server";
import {
createSessionsAllQuery,
datetimeFilterToPrismaSql,
traceException,
} from "@langfuse/shared/src/server";
const SessionFilterOptions = z.object({
projectId: z.string(), // Required for protectedProjectProcedure
filter: z.array(singleFilter).nullable(),
+7 -5
View File
@@ -9,14 +9,10 @@ import {
protectedProjectProcedure,
} from "@/src/server/api/trpc";
import {
datetimeFilterToPrisma,
datetimeFilterToPrismaSql,
filterAndValidateDbScoreList,
orderBy,
orderByToPrismaSql,
paginationZod,
singleFilter,
tableColumnsToSqlFilterAndPrefix,
timeFilter,
type TraceOptions,
tracesTableCols,
@@ -27,7 +23,13 @@ import {
Prisma,
type Trace,
} from "@langfuse/shared/src/db";
import { traceException } from "@langfuse/shared/src/server";
import {
datetimeFilterToPrisma,
datetimeFilterToPrismaSql,
orderByToPrismaSql,
tableColumnsToSqlFilterAndPrefix,
traceException,
} from "@langfuse/shared/src/server";
import { TRPCError } from "@trpc/server";
import type Decimal from "decimal.js";
+2 -5
View File
@@ -4,14 +4,11 @@ import {
createTRPCRouter,
protectedProjectProcedure,
} from "@/src/server/api/trpc";
import { paginationZod } from "@langfuse/shared";
import {
singleFilter,
tableColumnsToSqlFilterAndPrefix,
} from "@langfuse/shared";
import { paginationZod, singleFilter } from "@langfuse/shared";
import { Prisma } from "@langfuse/shared/src/db";
import { usersTableCols } from "@/src/server/api/definitions/usersTable";
import { type LastUserScore } from "@/src/features/scores/lib/types";
import { tableColumnsToSqlFilterAndPrefix } from "@langfuse/shared/src/server";
const UserFilterOptions = z.object({
projectId: z.string(), // Required for protectedProjectProcedure
+1 -1
View File
@@ -2,7 +2,6 @@ import {
type singleFilter,
type timeFilter,
type ColumnDefinition,
tableColumnsToSqlFilter,
} from "@langfuse/shared";
import { Prisma, type PrismaClient } from "@langfuse/shared/src/db";
import Decimal from "decimal.js";
@@ -14,6 +13,7 @@ import {
filterInterface,
} from "./sqlInterface";
import { tableDefinitions } from "./tableDefinitions";
import { tableColumnsToSqlFilter } from "@langfuse/shared/src/server";
export type InternalDatabaseRow = {
[key: string]: bigint | number | Decimal | string | Date;
+4 -3
View File
@@ -35,6 +35,7 @@ import {
traceException,
sendResetPasswordVerificationRequest,
instrumentAsync,
logger,
} from "@langfuse/shared/src/server";
import { getOrganizationPlan } from "@/src/features/entitlements/server/getOrganizationPlan";
import { projectRoleAccessRights } from "@/src/features/rbac/constants/projectAccessRights";
@@ -292,7 +293,7 @@ export async function getAuthOptions(): Promise<NextAuthOptions> {
try {
dynamicSsoProviders = await loadSsoProviders();
} catch (e) {
console.error("Error loading dynamic SSO providers", e);
logger.error("Error loading dynamic SSO providers", e);
traceException(e);
}
const providers = [...staticProviders, ...dynamicSsoProviders];
@@ -406,11 +407,11 @@ export async function getAuthOptions(): Promise<NextAuthOptions> {
// Block sign in without valid user.email
const email = user.email?.toLowerCase();
if (!email) {
console.error("No email found in user object");
logger.error("No email found in user object");
throw new Error("No email found in user object");
}
if (z.string().email().safeParse(email).success === false) {
console.error("Invalid email found in user object");
logger.error("Invalid email found in user object");
throw new Error("Invalid email found in user object");
}
+2 -6
View File
@@ -1,6 +1,6 @@
{
"name": "worker",
"version": "2.77.0",
"version": "2.78.0",
"description": "",
"license": "MIT",
"private": true,
@@ -22,7 +22,6 @@
"@appsignal/opentelemetry-instrumentation-bullmq": "^0.7.1",
"@clickhouse/client": "^1.4.0",
"@langfuse/shared": "workspace:*",
"@logtail/pino": "^0.5.0",
"@opentelemetry/api": "^1.8.0",
"@opentelemetry/auto-instrumentations-node": "^0.44.0",
"@opentelemetry/context-async-hooks": "^1.25.1",
@@ -30,8 +29,8 @@
"@opentelemetry/instrumentation-express": "^0.41.1",
"@opentelemetry/instrumentation-http": "^0.52.1",
"@opentelemetry/instrumentation-ioredis": "^0.42.0",
"@opentelemetry/instrumentation-pino": "^0.41.0",
"@opentelemetry/instrumentation-undici": "^0.4.0",
"@opentelemetry/instrumentation-winston": "^0.40.0",
"@prisma/instrumentation": "^5.13.0",
"backoff": "^2.5.0",
"bullmq": "^5.12.10",
@@ -48,9 +47,6 @@
"kysely": "^0.27.4",
"lodash": "^4.17.21",
"pg": "^8.11.5",
"pino": "^9.2.0",
"pino-http": "^10.2.0",
"pino-pretty": "^11.2.2",
"stripe": "^16.8.0",
"tiktoken": "^1.0.15",
"uuid": "^9.0.1",
+13 -16
View File
@@ -17,14 +17,14 @@ import {
import { encrypt } from "@langfuse/shared/encryption";
import { OpenAIServer } from "./network";
import { afterEach } from "node:test";
import { getEvalQueue } from "../queues/evalQueue";
import { logger } from "@langfuse/shared/src/server";
vi.mock("../redis/consumer", () => ({
evalQueue: {
add: vi.fn().mockImplementation((jobName, jobData) => {
console.log(
logger.info(
`Mock evalQueue.add called with jobName: ${jobName} and jobData:`,
jobData
jobData,
);
// Simulate the job being processed immediately by calling the job's processing function
// Note: You would replace `processJobFunction` with the actual function that processes the job
@@ -391,9 +391,6 @@ describe("create eval jobs", () => {
expect(jobs.length).toBe(1);
expect(jobs[0].project_id).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
expect(jobs[0].job_input_trace_id).toBe(traceId);
console.log(jobs[0]);
const j = await getEvalQueue()?.getJob(jobs[0].id);
console.log(j);
expect(jobs[0].status.toString()).toBe("CANCELLED");
expect(jobs[0].start_time).not.toBeNull();
expect(jobs[0].end_time).not.toBeNull();
@@ -594,8 +591,8 @@ describe("execute evals", () => {
await expect(evaluate({ event: payload })).rejects.toThrowError(
new LangfuseNotFoundError(
"API key for provider openai and project 7a88fb47-b4e2-43b8-a06c-a5ce950dc53a not found."
)
"API key for provider openai and project 7a88fb47-b4e2-43b8-a06c-a5ce950dc53a not found.",
),
);
const jobs = await kyselyPrisma.$kysely
@@ -730,7 +727,7 @@ describe("test variable extraction", () => {
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
["input", "output"],
traceId,
variableMapping
variableMapping,
);
expect(result).toEqual([
@@ -792,7 +789,7 @@ describe("test variable extraction", () => {
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
["input", "output"],
traceId,
variableMapping
variableMapping,
);
expect(result).toEqual([
@@ -842,12 +839,12 @@ describe("test variable extraction", () => {
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
["input", "output"],
traceId,
variableMapping
)
variableMapping,
),
).rejects.toThrowError(
new LangfuseNotFoundError(
`Observation great-llm-name for trace ${traceId} not found. Please ensure the mapped data exists and consider extending the job delay.`
)
`Observation great-llm-name for trace ${traceId} not found. Please ensure the mapped data exists and consider extending the job delay.`,
),
);
}, 10_000);
@@ -897,7 +894,7 @@ describe("test variable extraction", () => {
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
["input", "output"],
traceId,
variableMapping
variableMapping,
);
expect(result).toEqual([
@@ -973,7 +970,7 @@ describe("test variable extraction", () => {
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
["input", "output"],
traceId,
variableMapping
variableMapping,
);
expect(result).toEqual([
+6 -5
View File
@@ -1,5 +1,6 @@
import { setupServer } from "msw/node";
import { HttpResponse, http } from "msw";
import { logger } from "@langfuse/shared/src/server";
const DEFAULT_RESPONSE = {
id: "chatcmpl-9MhZ73aGSmhfAtjU9DwoL4om73hJ7",
@@ -32,7 +33,7 @@ const DEFAULT_RESPONSE = {
function CompletionHandler(response: HttpResponse) {
return http.post("https://api.openai.com/v1/chat/completions", async () => {
console.log("handler");
logger.info("handler");
return response;
});
}
@@ -46,7 +47,7 @@ function ErrorCompletionHandler(status: number, statusText: string) {
new HttpResponse(null, {
status,
statusText,
})
}),
);
}
@@ -64,15 +65,15 @@ export class OpenAIServer {
hasActiveKey?: boolean;
useDefaultResponse?: boolean;
}) {
console.log("openai", { hasActiveKey, useDefaultResponse });
logger.info("openai", { hasActiveKey, useDefaultResponse });
this.hasActiveKey = hasActiveKey;
this.internalServer = setupServer(
...(useDefaultResponse ? [JsonCompletionHandler(DEFAULT_RESPONSE)] : [])
...(useDefaultResponse ? [JsonCompletionHandler(DEFAULT_RESPONSE)] : []),
);
if (hasActiveKey) {
this.internalServer.events.on("response:bypass", async ({ response }) => {
console.log(response);
logger.info(response);
});
}
+1 -2
View File
@@ -1,8 +1,7 @@
import { prisma } from "@langfuse/shared/src/db";
import { env } from "../env";
import logger from "../logger";
import { logger } from "@langfuse/shared/src/server";
export const pruneDatabase = async () => {
if (!env.DATABASE_URL.includes("localhost:5432")) {
throw new Error("You cannot prune database unless running on localhost.");
+22 -11
View File
@@ -13,8 +13,7 @@ import {
import { env } from "../env";
import { checkContainerHealth } from "../features/health";
import logger from "../logger";
import { logger } from "@langfuse/shared/src/server";
const router = express.Router();
type EventsResponse = {
@@ -23,10 +22,22 @@ type EventsResponse = {
router.get<{}, { status: string }>("/health", async (_req, res) => {
try {
await checkContainerHealth(res);
await checkContainerHealth(res, false);
} catch (e) {
traceException(e);
logger.error(e, "Health check failed");
logger.error("Health check failed", e);
res.status(500).json({
status: "error",
});
}
});
router.get<{}, { status: string }>("/ready", async (_req, res) => {
try {
await checkContainerHealth(res, true);
} catch (e) {
traceException(e);
logger.error("Readiness check failed", e);
res.status(500).json({
status: "error",
});
@@ -48,16 +59,16 @@ router
});
logger.info(
`Clickhouse health check response: ${JSON.stringify(await response.text())}`
`Clickhouse health check response: ${JSON.stringify(await response.text())}`,
);
res.json({ status: "success" });
} catch (e) {
logger.error(e, "Clickhouse health check failed");
logger.error("Clickhouse health check failed", e);
res.status(500).json({ status: "error", message: JSON.stringify(e) });
}
} catch (e) {
logger.error(e, "Unexpected error during Clickhouse health check");
logger.error("Unexpected error during Clickhouse health check", e);
res.status(500).json({ status: "error", message: JSON.stringify(e) });
}
});
@@ -66,7 +77,7 @@ router
.use(
basicAuth({
users: { admin: env.LANGFUSE_WORKER_PASSWORD },
})
}),
)
.post<{}, EventsResponse>("/events", async (req, res) => {
try {
@@ -93,7 +104,7 @@ router
if (traceUpsertQueue) {
logger.info(
`Added ${jobs.length} trace upsert jobs to the queue`,
jobs
jobs,
);
}
@@ -117,7 +128,7 @@ router
return res.status(400).send();
} catch (e) {
logger.error(e, "Error processing events");
logger.error("Error processing events", e);
traceException(e);
return res.status(500).json({
status: "error",
@@ -129,7 +140,7 @@ router
.use(
basicAuth({
users: { admin: env.LANGFUSE_WORKER_PASSWORD },
})
}),
)
.post("/ingestion", async (req, res) => {
return res.status(200).send(); // Not implemented, Send 200 to acknowledge the request for web containers to not throw
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.77.0";
export const VERSION = "v2.78.0";
@@ -2,7 +2,7 @@ import { parseDbOrg, Prisma } from "@langfuse/shared";
import { prisma } from "@langfuse/shared/src/db";
import Stripe from "stripe";
import { env } from "../../env";
import logger from "../../logger";
import { logger } from "@langfuse/shared/src/server";
import {
cloudUsageMeteringDbCronJobName,
CloudUsageMeteringDbCronJobStates,
@@ -36,7 +36,7 @@ export const handleCloudUsageMeteringJob = async (job: Job) => {
}
if (cron.lastRun.getTime() % 3600000 !== 0) {
throw new Error(
"Cloud Usage Metering Cron Job last run is not on the full hour"
"Cloud Usage Metering Cron Job last run is not on the full hour",
);
}
if (cron.lastRun.getTime() + delayFromStartOfInterval > Date.now()) {
@@ -50,7 +50,7 @@ export const handleCloudUsageMeteringJob = async (job: Job) => {
cron.jobStartedAt < new Date(Date.now() - 1200000)
) {
logger.warn(
"Last cloud usage metering job started at is older than 20 minutes, retrying job"
"Last cloud usage metering job started at is older than 20 minutes, retrying job",
);
} else {
logger.warn("Cloud Usage Metering Job already in progress");
@@ -70,7 +70,7 @@ export const handleCloudUsageMeteringJob = async (job: Job) => {
const meterIntervalStart = cron.lastRun;
const meterIntervalEnd = new Date(cron.lastRun.getTime() + 3600000);
logger.info(
`Cloud Usage Metering Job running for interval ${meterIntervalStart.toISOString()} - ${meterIntervalEnd.toISOString()}`
`Cloud Usage Metering Job running for interval ${meterIntervalStart.toISOString()} - ${meterIntervalEnd.toISOString()}`,
);
// find all organizations which have a stripe org id set up
@@ -85,7 +85,7 @@ export const handleCloudUsageMeteringJob = async (job: Job) => {
})
).map(parseDbOrg);
logger.info(
`Cloud Usage Metering Job for ${organizations.length} organizations`
`Cloud Usage Metering Job for ${organizations.length} organizations`,
);
// setup stripe client
@@ -118,7 +118,7 @@ export const handleCloudUsageMeteringJob = async (job: Job) => {
},
});
logger.info(
`Cloud Usage Metering Job for org ${org.id} - ${stripeCustomerId} stripe customer id - ${countObservations} observations`
`Cloud Usage Metering Job for org ${org.id} - ${stripeCustomerId} stripe customer id - ${countObservations} observations`,
);
if (countObservations > 0) {
@@ -144,7 +144,7 @@ export const handleCloudUsageMeteringJob = async (job: Job) => {
countProcessedObservations,
{
unit: "observations",
}
},
);
// update cron job
-4
View File
@@ -42,9 +42,6 @@ const EnvSchema = z.object({
.number()
.positive()
.default(3),
LANGFUSE_LOG_LEVEL: z
.enum(["trace", "debug", "info", "warn", "error", "fatal"])
.optional(),
REDIS_HOST: z.string().nullish(),
REDIS_PORT: z.coerce
.number({
@@ -61,7 +58,6 @@ const EnvSchema = z.object({
CLICKHOUSE_URL: z.string().url().optional(),
CLICKHOUSE_USER: z.string().optional(),
CLICKHOUSE_PASSWORD: z.string().optional(),
LANGFUSE_WORKER_BETTERSTACK_TOKEN: z.string().optional(),
LANGFUSE_LEGACY_INGESTION_WORKER_CONCURRENCY: z.coerce
.number()
.positive()
@@ -1,6 +1,6 @@
import { BatchExportStatus } from "@langfuse/shared";
import { kyselyPrisma } from "@langfuse/shared/src/db";
import logger from "../../logger";
import { logger } from "@langfuse/shared/src/server";
import {
traceException,
getBatchExportQueue,
@@ -35,7 +35,7 @@ export async function enqueueBatchExportJobs() {
projectId: job.project_id,
},
},
}) as const
}) as const,
);
await queue.addBulk(newJobs);
@@ -44,7 +44,7 @@ export async function enqueueBatchExportJobs() {
} catch (error) {
logger.error(
"Error while checking for QUEUED batch export jobs in postgres",
error
error,
);
traceException(error);
}
@@ -6,23 +6,22 @@ import {
BatchExportStatus,
exportOptions,
FilterCondition,
createSessionsAllQuery,
Prisma,
} from "@langfuse/shared";
import { prisma } from "@langfuse/shared/src/db";
import {
DatabaseReadStream,
S3StorageService,
createSessionsAllQuery,
sendBatchExportSuccessEmail,
streamTransformations,
BatchExportJobType,
} from "@langfuse/shared/src/server";
import { env } from "../../env";
import logger from "../../logger";
import { logger } from "@langfuse/shared/src/server";
export const handleBatchExportJob = async (
batchExportJob: BatchExportJobType
batchExportJob: BatchExportJobType,
) => {
const { projectId, batchExportId } = batchExportJob;
@@ -96,7 +95,7 @@ export const handleBatchExportJob = async (
orderBy,
limit: pageSize,
page: Math.floor(offset / pageSize),
}
},
);
const chunk = await prisma.$queryRaw<unknown[]>(query);
@@ -104,7 +103,7 @@ export const handleBatchExportJob = async (
return chunk;
},
1000,
env.BATCH_EXPORT_ROW_LIMIT
env.BATCH_EXPORT_ROW_LIMIT,
);
// Transform data to desired format
@@ -113,9 +112,9 @@ export const handleBatchExportJob = async (
streamTransformations[jobDetails.format as BatchExportFileFormat](),
(err) => {
if (err) {
console.error("Getting data from DB and transform failed: ", err);
logger.error("Getting data from DB and transform failed: ", err);
}
}
},
);
// Stream upload results to S3
+38 -34
View File
@@ -7,6 +7,7 @@ import {
QueueName,
EvalExecutionEvent,
TraceUpsertEventSchema,
tableColumnsToSqlFilterAndPrefix,
} from "@langfuse/shared/src/server";
import {
ApiError,
@@ -19,7 +20,6 @@ import {
LLMApiKeySchema,
Prisma,
singleFilter,
tableColumnsToSqlFilterAndPrefix,
InvalidRequestError,
variableMappingList,
ZodModelConfig,
@@ -27,7 +27,7 @@ import {
import { decrypt } from "@langfuse/shared/encryption";
import { kyselyPrisma, prisma } from "@langfuse/shared/src/db";
import logger from "../../logger";
import { logger } from "@langfuse/shared/src/server";
import { getEvalQueue } from "../../queues/evalQueue";
// this function is used to determine which eval jobs to create for a given trace
@@ -49,7 +49,7 @@ export const createEvalJobs = async ({
return;
}
logger.info(
`Creating eval jobs for trace ${event.traceId} on project ${event.projectId}`
`Creating eval jobs for trace ${event.traceId} on project ${event.projectId}`,
);
for (const config of configs) {
@@ -64,7 +64,7 @@ export const createEvalJobs = async ({
const condition = tableColumnsToSqlFilterAndPrefix(
validatedFilter,
evalTableCols,
"traces"
"traces",
);
const joinedQuery = Prisma.sql`
@@ -88,7 +88,7 @@ export const createEvalJobs = async ({
// if we matched a trace, we might want to create a job
if (traces.length > 0) {
logger.info(
`Eval job for config ${config.id} matched trace ids ${JSON.stringify(traces.map((t) => t.id))}`
`Eval job for config ${config.id} matched trace ids ${JSON.stringify(traces.map((t) => t.id))}`,
);
const jobExecutionId = randomUUID();
@@ -96,7 +96,7 @@ export const createEvalJobs = async ({
// deduplication: if a job exists already for a trace event, we do not create a new one.
if (existingJob.length > 0) {
logger.info(
`Eval job for config ${config.id} and trace ${event.traceId} already exists`
`Eval job for config ${config.id} and trace ${event.traceId} already exists`,
);
continue;
}
@@ -108,14 +108,14 @@ export const createEvalJobs = async ({
const random = Math.random();
if (random > parseFloat(config.sampling)) {
logger.info(
`Eval job for config ${config.id} and trace ${event.traceId} was sampled out`
`Eval job for config ${config.id} and trace ${event.traceId} was sampled out`,
);
continue;
}
}
logger.info(
`Creating eval job for config ${config.id} and trace ${event.traceId}`
`Creating eval job for config ${config.id} and trace ${event.traceId}`,
);
await prisma.jobExecution.create({
@@ -150,7 +150,7 @@ export const createEvalJobs = async ({
delay: config.delay, // milliseconds
removeOnComplete: true,
removeOnFail: 1_000,
}
},
);
} else {
// if we do not have a match, and execution exists, we mark the job as cancelled
@@ -158,7 +158,7 @@ export const createEvalJobs = async ({
logger.info(`Eval job for config ${config.id} did not match trace`);
if (existingJob.length > 0) {
logger.info(
`Cancelling eval job for config ${config.id} and trace ${event.traceId}`
`Cancelling eval job for config ${config.id} and trace ${event.traceId}`,
);
await kyselyPrisma.$kysely
.updateTable("job_executions")
@@ -178,7 +178,7 @@ export const evaluate = async ({
event: z.infer<typeof EvalExecutionEvent>;
}) => {
logger.info(
`Evaluating job ${event.jobExecutionId} for project ${event.projectId}`
`Evaluating job ${event.jobExecutionId} for project ${event.projectId}`,
);
// first, fetch all the context required for the evaluation
const job = await kyselyPrisma.$kysely
@@ -219,12 +219,12 @@ export const evaluate = async ({
.executeTakeFirstOrThrow();
logger.info(
`Evaluating job ${job.id} for project ${event.projectId} with template ${template.id}. Searching for context...`
`Evaluating job ${job.id} for project ${event.projectId} with template ${template.id}. Searching for context...`,
);
// selectedcolumnid is not safe to use, needs validation in extractVariablesFromTrace()
const parsedVariableMapping = variableMappingList.parse(
config.variable_mapping
config.variable_mapping,
);
// extract the variables which need to be inserted into the prompt
@@ -232,22 +232,22 @@ export const evaluate = async ({
event.projectId,
template.vars,
job.job_input_trace_id,
parsedVariableMapping
parsedVariableMapping,
);
logger.info(
`Evaluating job ${event.jobExecutionId} extracted variables ${JSON.stringify(mappingResult)} `
`Evaluating job ${event.jobExecutionId} extracted variables ${JSON.stringify(mappingResult)} `,
);
// compile the prompt and send out the LLM request
const prompt = compileHandlebarString(template.prompt, {
...Object.fromEntries(
mappingResult.map(({ var: key, value }) => [key, value])
mappingResult.map(({ var: key, value }) => [key, value]),
),
});
logger.info(
`Evaluating job ${event.jobExecutionId} compiled prompt ${prompt}`
`Evaluating job ${event.jobExecutionId} compiled prompt ${prompt}`,
);
const parsedOutputSchema = z
@@ -280,10 +280,10 @@ export const evaluate = async ({
if (!parsedKey.success) {
// this will fail the eval execution if a user deletes the API key.
logger.error(
`Evaluating job ${event.jobExecutionId} did not find API key for provider ${template.provider} and project ${event.projectId}. Eval will fail. ${parsedKey.error}`
`Evaluating job ${event.jobExecutionId} did not find API key for provider ${template.provider} and project ${event.projectId}. Eval will fail. ${parsedKey.error}`,
);
throw new LangfuseNotFoundError(
`API key for provider ${template.provider} and project ${event.projectId} not found.`
`API key for provider ${template.provider} and project ${event.projectId} not found.`,
);
}
@@ -313,7 +313,7 @@ export const evaluate = async ({
const parsedLLMOutput = openAIFunction.parse(completion);
logger.info(
`Evaluating job ${event.jobExecutionId} Parsed LLM output ${JSON.stringify(parsedLLMOutput)}`
`Evaluating job ${event.jobExecutionId} Parsed LLM output ${JSON.stringify(parsedLLMOutput)}`,
);
// persist the score and update the job status
@@ -332,7 +332,7 @@ export const evaluate = async ({
});
logger.info(
`Evaluating job ${event.jobExecutionId} persisted score ${scoreId} for trace ${job.job_input_trace_id}`
`Evaluating job ${event.jobExecutionId} persisted score ${scoreId} for trace ${job.job_input_trace_id}`,
);
await kyselyPrisma.$kysely
@@ -344,13 +344,13 @@ export const evaluate = async ({
.execute();
logger.info(
`Eval job ${job.id} for project ${event.projectId} completed with score ${parsedLLMOutput.score}`
`Eval job ${job.id} for project ${event.projectId} completed with score ${parsedLLMOutput.score}`,
);
};
export function compileHandlebarString(
handlebarString: string,
context: Record<string, any>
context: Record<string, any>,
): string {
const template = Handlebars.compile(handlebarString, { noEscape: true });
return template(context);
@@ -361,14 +361,14 @@ export async function extractVariablesFromTrace(
variables: string[],
traceId: string,
// this here are variables which were inserted by users. Need to validate before DB query.
variableMapping: z.infer<typeof variableMappingList>
variableMapping: z.infer<typeof variableMappingList>,
) {
const mappingResult: { var: string; value: string }[] = [];
// find the context for each variable of the template
for (const variable of variables) {
const mapping = variableMapping.find(
(m) => m.templateVariable === variable
(m) => m.templateVariable === variable,
);
if (!mapping) {
@@ -386,7 +386,7 @@ export async function extractVariablesFromTrace(
// if no column was found, we still process with an empty variable
if (!safeInternalColumn?.id) {
logger.error(
`No column found for variable ${variable} and column ${mapping.selectedColumnId}`
`No column found for variable ${variable} and column ${mapping.selectedColumnId}`,
);
mappingResult.push({ var: variable, value: "" });
continue;
@@ -395,7 +395,9 @@ export async function extractVariablesFromTrace(
const trace = await kyselyPrisma.$kysely
.selectFrom("traces as t")
.select(
sql`${sql.raw(safeInternalColumn.internal)}`.as(safeInternalColumn.id)
sql`${sql.raw(safeInternalColumn.internal)}`.as(
safeInternalColumn.id,
),
) // query the internal column name raw
.where("id", "=", traceId)
.where("project_id", "=", projectId)
@@ -404,10 +406,10 @@ export async function extractVariablesFromTrace(
// user facing errors
if (!trace) {
logger.error(
`Trace ${traceId} for project ${projectId} not found. Eval will succeed without trace input. Please ensure the mapped data on the trace exists and consider extending the job delay.`
`Trace ${traceId} for project ${projectId} not found. Eval will succeed without trace input. Please ensure the mapped data on the trace exists and consider extending the job delay.`,
);
throw new LangfuseNotFoundError(
`Trace ${traceId} for project ${projectId} not found. Eval will succeed without trace input. Please ensure the mapped data on the trace exists and consider extending the job delay.`
`Trace ${traceId} for project ${projectId} not found. Eval will succeed without trace input. Please ensure the mapped data on the trace exists and consider extending the job delay.`,
);
}
@@ -423,7 +425,7 @@ export async function extractVariablesFromTrace(
if (!mapping.objectName) {
logger.info(
`No object name found for variable ${variable} and object ${mapping.langfuseObject}`
`No object name found for variable ${variable} and object ${mapping.langfuseObject}`,
);
mappingResult.push({ var: variable, value: "" });
continue;
@@ -431,7 +433,7 @@ export async function extractVariablesFromTrace(
if (!safeInternalColumn?.id) {
logger.warn(
`No column found for variable ${variable} and column ${mapping.selectedColumnId}`
`No column found for variable ${variable} and column ${mapping.selectedColumnId}`,
);
mappingResult.push({ var: variable, value: "" });
continue;
@@ -440,7 +442,9 @@ export async function extractVariablesFromTrace(
const observation = await kyselyPrisma.$kysely
.selectFrom("observations as o")
.select(
sql`${sql.raw(safeInternalColumn.internal)}`.as(safeInternalColumn.id)
sql`${sql.raw(safeInternalColumn.internal)}`.as(
safeInternalColumn.id,
),
) // query the internal column name raw
.where("trace_id", "=", traceId)
.where("project_id", "=", projectId)
@@ -451,10 +455,10 @@ export async function extractVariablesFromTrace(
// user facing errors
if (!observation) {
logger.error(
`Observation ${mapping.objectName} for trace ${traceId} not found. Please ensure the mapped data exists and consider extending the job delay.`
`Observation ${mapping.objectName} for trace ${traceId} not found. Please ensure the mapped data exists and consider extending the job delay.`,
);
throw new LangfuseNotFoundError(
`Observation ${mapping.objectName} for trace ${traceId} not found. Please ensure the mapped data exists and consider extending the job delay.`
`Observation ${mapping.objectName} for trace ${traceId} not found. Please ensure the mapped data exists and consider extending the job delay.`,
);
}
+13 -7
View File
@@ -1,12 +1,18 @@
import { prisma } from "@langfuse/shared/src/db";
import { redis } from "@langfuse/shared/src/server";
import { logger, redis } from "@langfuse/shared/src/server";
import { Response } from "express";
import logger from "../../logger";
export const checkContainerHealth = async (res: Response) => {
if (isSigtermReceived()) {
/**
* Check the health of the container.
* If failOnSigterm is true, the health check will fail if a SIGTERM signal has been received.
*/
export const checkContainerHealth = async (
res: Response,
failOnSigterm: boolean,
) => {
if (failOnSigterm && isSigtermReceived()) {
logger.info(
"Health check failed: SIGTERM / SIGINT received, shutting down."
"Health check failed: SIGTERM / SIGINT received, shutting down.",
);
return res.status(500).json({
status: "SIGTERM / SIGINT received, shutting down",
@@ -25,8 +31,8 @@ export const checkContainerHealth = async (res: Response) => {
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Redis ping timeout after 2 seconds")),
2000
)
2000,
),
),
]);
+13 -13
View File
@@ -10,7 +10,7 @@ import {
} from "tiktoken";
import { z } from "zod";
import { instrumentSync } from "@langfuse/shared/src/server";
import { instrumentSync, logger } from "@langfuse/shared/src/server";
const OpenAiTokenConfig = z.object({
tokenizerModel: z.string().refine(isTiktokenModel, {
@@ -54,12 +54,12 @@ export function tokenCount(p: {
return claudeTokenCount(p.text);
} else {
if (p.model.tokenizerId) {
console.error(`Unknown tokenizer ${p.model.tokenizerId}`);
logger.error(`Unknown tokenizer ${p.model.tokenizerId}`);
}
return undefined;
}
}
},
);
}
@@ -72,10 +72,10 @@ type ChatMessage = {
function openAiTokenCount(p: { model: Model; text: unknown }) {
const config = OpenAiTokenConfig.safeParse(p.model.tokenizerConfig);
if (!config.success) {
console.error(
logger.error(
`Invalid tokenizer config for model ${p.model.id}: ${JSON.stringify(
p.model.tokenizerConfig
)}, ${JSON.stringify(config.error)}`
p.model.tokenizerConfig,
)}, ${JSON.stringify(config.error)}`,
);
return undefined;
}
@@ -90,13 +90,13 @@ function openAiTokenCount(p: { model: Model; text: unknown }) {
) {
// check if the tokenizerConfig is a valid chat config
const parsedConfig = OpenAiChatTokenConfig.safeParse(
p.model.tokenizerConfig
p.model.tokenizerConfig,
);
if (!parsedConfig.success) {
console.error(
logger.error(
`Invalid tokenizer config for chat model ${
p.model.id
}: ${JSON.stringify(p.model.tokenizerConfig)}`
}: ${JSON.stringify(p.model.tokenizerConfig)}`,
);
return undefined;
}
@@ -109,7 +109,7 @@ function openAiTokenCount(p: { model: Model; text: unknown }) {
? getTokensByModel(config.data.tokenizerModel, parsedText)
: getTokensByModel(
config.data.tokenizerModel,
JSON.stringify(parsedText)
JSON.stringify(parsedText),
);
}
return result;
@@ -161,7 +161,7 @@ function openAiChatTokenCount(params: {
}
const getTokensByModel = (model: TiktokenModel, text: string) => {
// encoiding should be kept in memory to avoid re-creating it
// encoding should be kept in memory to avoid re-creating it
let encoding: Tiktoken | undefined;
try {
cachedTokenizerByModel[model] =
@@ -169,7 +169,7 @@ const getTokensByModel = (model: TiktokenModel, text: string) => {
encoding = cachedTokenizerByModel[model];
} catch (KeyError) {
console.log("Warning: model not found. Using cl100k_base encoding.");
logger.warn("Model not found. Using cl100k_base encoding.");
encoding = get_encoding("cl100k_base");
}
@@ -205,7 +205,7 @@ function isChatMessageArray(value: unknown): value is ChatMessage[] {
typeof item.role === "string" &&
"content" in item &&
typeof item.content === "string" &&
(!("name" in item) || typeof item.name === "string")
(!("name" in item) || typeof item.name === "string"),
);
}
+2 -1
View File
@@ -1,7 +1,8 @@
import { logger } from "@langfuse/shared/src/server";
import "./instrumentation"; // instrumenting the application
import app from "./app";
import { env } from "./env";
import logger from "./logger";
export const server = app.listen(env.PORT, () => {
logger.info(`Listening: http://localhost:${env.PORT}`);
+2 -2
View File
@@ -3,12 +3,12 @@ import { registerInstrumentations } from "@opentelemetry/instrumentation";
import { IORedisInstrumentation } from "@opentelemetry/instrumentation-ioredis";
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
import { PinoInstrumentation } from "@opentelemetry/instrumentation-pino";
import { PrismaInstrumentation } from "@prisma/instrumentation";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks";
import opentelemetry from "@opentelemetry/api";
import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
import { WinstonInstrumentation } from "@opentelemetry/instrumentation-winston";
// import { BullMQInstrumentation } from "@appsignal/opentelemetry-instrumentation-bullmq";
const contextManager = new AsyncHooksContextManager().enable();
@@ -28,8 +28,8 @@ registerInstrumentations({
new HttpInstrumentation(),
new ExpressInstrumentation(),
new PrismaInstrumentation(),
new WinstonInstrumentation({ disableLogSending: true }),
getNodeAutoInstrumentations(),
new PinoInstrumentation(),
new UndiciInstrumentation(),
// new BullMQInstrumentation(),
],
-33
View File
@@ -1,33 +0,0 @@
import pino from "pino";
import { env } from "./env";
const getBetterstackLogginTransport = (minLevel: string) => {
return env.LANGFUSE_WORKER_BETTERSTACK_TOKEN
? pino.transport({
target: "@logtail/pino",
options: { sourceToken: env.LANGFUSE_WORKER_BETTERSTACK_TOKEN },
level: minLevel,
})
: { level: minLevel };
};
export const getLogger = (
env: "development" | "production" | "test",
minLevel = "info"
) => {
if (env === "production") {
return pino(getBetterstackLogginTransport(minLevel));
}
return pino({
level: minLevel,
transport: {
target: "pino-pretty",
options: {
translateTime: "HH:MM:ss Z",
ignore: "pid,hostname",
},
},
});
};
const logger = getLogger(env.NODE_ENV, env.LANGFUSE_LOG_LEVEL);
export default logger;
+6 -3
View File
@@ -3,8 +3,11 @@ import { Job } from "bullmq";
import { BaseError, BatchExportStatus } from "@langfuse/shared";
import { kyselyPrisma } from "@langfuse/shared/src/db";
import { traceException, instrumentAsync } from "@langfuse/shared/src/server";
import logger from "../logger";
import {
traceException,
instrumentAsync,
logger,
} from "@langfuse/shared/src/server";
import { QueueName, TQueueJobTypes } from "@langfuse/shared/src/server";
import { handleBatchExportJob } from "../features/batchExport/handleBatchExportJob";
import { SpanKind } from "@opentelemetry/api";
@@ -39,8 +42,8 @@ export const batchExportQueueProcessor = async (
.execute();
logger.error(
`Failed Batch Export job for id ${job.data.payload.batchExportId}`,
e,
`Failed Batch Export job for id ${job.data.payload.batchExportId} ${e}`,
);
traceException(e);
throw e;
+1 -1
View File
@@ -1,7 +1,7 @@
import { Processor, Queue } from "bullmq";
import logger from "../logger";
import {
redis,
logger,
QueueName,
QueueJobs,
instrumentAsync,
+3 -3
View File
@@ -2,12 +2,12 @@ import { Job, Queue } from "bullmq";
import { ApiError, BaseError } from "@langfuse/shared";
import { evaluate, createEvalJobs } from "../features/evaluation/eval-service";
import { kyselyPrisma } from "@langfuse/shared/src/db";
import logger from "../logger";
import { sql } from "kysely";
import {
createNewRedisInstance,
QueueName,
TQueueJobTypes,
logger,
traceException,
instrumentAsync,
recordIncrement,
@@ -77,8 +77,8 @@ export const evalJobCreatorQueueProcessor = async (
return true;
} catch (e) {
logger.error(
`Failed job Evaluation for traceId ${job.data.payload.traceId}`,
e,
`Failed job Evaluation for traceId ${job.data.payload.traceId} ${e}`,
);
traceException(e);
throw e;
@@ -148,8 +148,8 @@ export const evalJobExecutorQueueProcessor = async (
) {
traceException(e);
logger.error(
`Failed Evaluation_Execution job for id ${job.data.payload.jobExecutionId}`,
e,
`Failed Evaluation_Execution job for id ${job.data.payload.jobExecutionId} ${e}`,
);
}
@@ -4,13 +4,13 @@ import { redis, QueueJobs } from "@langfuse/shared/src/server";
import { prisma } from "@langfuse/shared/src/db";
import {
clickhouseClient,
logger,
getIngestionFlushQueue,
instrumentAsync,
recordIncrement,
recordGauge,
recordHistogram,
} from "@langfuse/shared/src/server";
import logger from "../logger";
import { ClickhouseWriter } from "../services/ClickhouseWriter";
import { IngestionService } from "../services/IngestionService";
import { SpanKind } from "@opentelemetry/api";
+2 -2
View File
@@ -8,8 +8,8 @@ import {
recordGauge,
recordHistogram,
TQueueJobTypes,
logger,
} from "@langfuse/shared/src/server";
import logger from "../logger";
import {
handleBatch,
@@ -75,8 +75,8 @@ export const legacyIngestionQueueProcessor: Processor = async (
);
} catch (e) {
logger.error(
`Failed job Evaluation for traceId ${job.data.payload}`,
e,
`Failed job Evaluation for traceId ${job.data.payload} ${e}`,
);
traceException(e);
throw e;
+2 -4
View File
@@ -1,7 +1,5 @@
import logger from "../logger";
import { Job, Processor, Worker, WorkerOptions } from "bullmq";
import { createNewRedisInstance } from "@langfuse/shared/src/server";
import { logger, createNewRedisInstance } from "@langfuse/shared/src/server";
export class WorkerManager {
private static workers: { [key: string]: Worker } = {};
@@ -52,8 +50,8 @@ export class WorkerManager {
// Add error handling
worker.on("failed", (job: Job | undefined, err: Error) => {
logger.error(
`Queue Job ${job?.name} with id ${job?.id} in ${queueName} failed`,
err,
`Queue Job ${job?.name} with id ${job?.id} in ${queueName} failed with error ${err}`,
);
});
worker.on("error", (failedReason: Error) => {
+64 -63
View File
@@ -4,6 +4,7 @@ import { prisma, Prisma } from "@langfuse/shared/src/db";
import {
clickhouseClient,
clickhouseStringDateSchema,
logger,
} from "@langfuse/shared/src/server";
// Constants
@@ -20,7 +21,7 @@ main({
overwriteTraceIds: [],
overwriteScoreIds: [],
}).then(() => {
console.log("done");
logger.info("done");
process.exit(0);
});
@@ -53,7 +54,7 @@ async function main(params: MainParams) {
let currentIteration = 0;
while (currentIteration < ITERATIONS) {
console.log(`Iteration ${currentIteration + 1}/${ITERATIONS}`);
logger.info(`Iteration ${currentIteration + 1}/${ITERATIONS}`);
// Check observations
let observations: unknown[] = [];
@@ -67,7 +68,7 @@ async function main(params: MainParams) {
observations
WHERE
id IN (${Prisma.join(overwriteObservationIds, ", ")})
`
`,
);
} else if (!hasOverwrite) {
const randomObservations = await prisma.$queryRaw<unknown[]>(
@@ -80,13 +81,13 @@ async function main(params: MainParams) {
start_time > ${DATE_START}::TIMESTAMP WITH time zone at time zone 'UTC'
AND start_time < ${DATE_END}::TIMESTAMP WITH time zone at time zone 'UTC'
LIMIT ${LIMIT}
`
`,
);
observations = randomObservations;
}
console.log(`Verifying ${observations.length} observations...`);
logger.info(`Verifying ${observations.length} observations...`);
try {
const results = await Promise.allSettled(
@@ -99,7 +100,7 @@ async function main(params: MainParams) {
checkedObservationSet.add(id);
await verifyClickhouseObservation(obs);
})
}),
);
results.forEach((result, i) => {
@@ -107,11 +108,11 @@ async function main(params: MainParams) {
const message = `[${i}] ` + result.reason;
failedObservationList.push(message);
console.error(message);
logger.error(message);
}
});
} catch (e) {
console.error(e);
logger.error(e);
}
// Check traces
@@ -123,7 +124,7 @@ async function main(params: MainParams) {
FROM traces
WHERE
id IN (${Prisma.join(overwriteTraceIds, ", ")})
`
`,
);
} else if (!hasOverwrite) {
const randomTraces = await prisma.$queryRaw<unknown[]>(
@@ -134,13 +135,13 @@ async function main(params: MainParams) {
timestamp > ${DATE_START}::TIMESTAMP WITH time zone at time zone 'UTC'
AND timestamp < ${DATE_END}::TIMESTAMP WITH time zone at time zone 'UTC'
LIMIT ${LIMIT}
`
`,
);
traces = randomTraces;
}
console.log(`Verifying ${traces.length} traces...`);
logger.info(`Verifying ${traces.length} traces...`);
try {
const results = await Promise.allSettled(
@@ -153,7 +154,7 @@ async function main(params: MainParams) {
checkedTraceSet.add(id);
await verifyClickhouseTrace(trace);
})
}),
);
results.forEach((result, i) => {
@@ -161,11 +162,11 @@ async function main(params: MainParams) {
const message = `[${i}] ` + result.reason;
failedTraceList.push(message);
console.error(message);
logger.error(message);
}
});
} catch (e) {
console.error(e);
logger.error(e);
}
// Check scores
@@ -177,7 +178,7 @@ async function main(params: MainParams) {
FROM scores
WHERE
id IN (${Prisma.join(overwriteScoreIds, ", ")})
`
`,
);
} else if (!hasOverwrite) {
const randomScores = await prisma.$queryRaw<unknown[]>(
@@ -189,13 +190,13 @@ async function main(params: MainParams) {
AND timestamp < ${DATE_END}::TIMESTAMP WITH time zone at time zone 'UTC'
AND source = 'API'
LIMIT ${LIMIT}
`
`,
);
scores = randomScores;
}
console.log(`Verifying ${scores.length} scores...`);
logger.info(`Verifying ${scores.length} scores...`);
try {
const results = await Promise.allSettled(
@@ -209,7 +210,7 @@ async function main(params: MainParams) {
checkedScoreSet.add(id);
await verifyClickhouseScore(score);
})
}),
);
results.forEach((result, i) => {
@@ -217,59 +218,59 @@ async function main(params: MainParams) {
const message = `[${i}] ` + result.reason;
failedScoreList.push(message);
console.error(message);
logger.error(message);
}
});
} catch (e) {
console.error(e);
logger.error(e);
}
currentIteration++;
if (hasOverwrite) break;
}
console.log(`Total observations verified: ${totalObservationCount}`);
console.log(`Total traces verified: ${totalTraceCount}`);
console.log(`Total scores verified: ${totalScoreCount}`);
logger.info(`Total observations verified: ${totalObservationCount}`);
logger.info(`Total traces verified: ${totalTraceCount}`);
logger.info(`Total scores verified: ${totalScoreCount}`);
if (failedObservationList.length > 0) {
await writeFile(
`src/scripts/output/${new Date().toISOString()}_failedObservations.txt`,
failedObservationList.join("\n")
failedObservationList.join("\n"),
),
console.error(
`Failed to verify ${failedObservationList.length} out of ${totalObservationCount} observations`
logger.error(
`Failed to verify ${failedObservationList.length} out of ${totalObservationCount} observations`,
);
} else {
console.log(
`All ${totalObservationCount} observations verified successfully`
logger.info(
`All ${totalObservationCount} observations verified successfully`,
);
}
if (failedTraceList.length > 0) {
await writeFile(
`src/scripts/output/${new Date().toISOString()}_failedTraces.txt`,
failedTraceList.join("\n")
failedTraceList.join("\n"),
);
console.error(
`Failed to verify ${failedTraceList.length} out of ${totalTraceCount} traces`
logger.error(
`Failed to verify ${failedTraceList.length} out of ${totalTraceCount} traces`,
);
} else {
console.log(`All ${totalTraceCount} traces verified successfully`);
logger.info(`All ${totalTraceCount} traces verified successfully`);
}
if (failedScoreList.length > 0) {
await writeFile(
`src/scripts/output/${new Date().toISOString()}_failedScores.txt`,
failedScoreList.join("\n")
failedScoreList.join("\n"),
);
console.error(
`Failed to verify ${failedScoreList.length} out of ${totalScoreCount} scores`
logger.error(
`Failed to verify ${failedScoreList.length} out of ${totalScoreCount} scores`,
);
} else {
console.log(`All ${totalScoreCount} scores verified successfully`);
logger.info(`All ${totalScoreCount} scores verified successfully`);
}
}
@@ -283,7 +284,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
const clickhouseRecord = (await clickhouseResult.json())[0];
if (!clickhouseRecord) {
throw new Error(
`Observation ${observationId} not found in Clickhouse for project ${projectId}`
`Observation ${observationId} not found in Clickhouse for project ${projectId}`,
);
}
@@ -366,7 +367,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
Object.entries(chValue).map(([k, v]) => [
k,
JSON.parse(v as any),
])
]),
);
if (
@@ -375,11 +376,11 @@ async function verifyClickhouseObservation(postgresObservation: any) {
parsedChMetadata,
typeof parsedChMetadata === "string"
? undefined
: Object.keys(parsedChMetadata as any).sort()
: Object.keys(parsedChMetadata as any).sort(),
)
) {
throw new Error(
getErrorMessage({ key, pgValue, chValue: parsedChMetadata })
getErrorMessage({ key, pgValue, chValue: parsedChMetadata }),
);
}
@@ -403,7 +404,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
JSON.stringify(parsedPgValue, Object.keys(parsedPgValue).sort()) !==
JSON.stringify(
parsedChValue,
Object.keys(parsedChValue as any).sort()
Object.keys(parsedChValue as any).sort(),
)
) {
throw new Error(
@@ -411,7 +412,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
key,
pgValue: JSON.stringify(parsedPgValue),
chValue: JSON.stringify(parsedChValue),
})
}),
);
}
@@ -421,7 +422,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
case "modelParameters": {
const parsedPgValue = pgValue;
const parsedChValue = JSON.parse(
(clickhouseRecord as any)["model_parameters"]
(clickhouseRecord as any)["model_parameters"],
);
if (
@@ -430,7 +431,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
JSON.stringify(parsedChValue, Object.keys(parsedChValue).sort())
) {
throw new Error(
getErrorMessage({ key, pgValue, chValue: parsedChValue })
getErrorMessage({ key, pgValue, chValue: parsedChValue }),
);
}
@@ -447,7 +448,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
key,
pgValue,
chValue: (clickhouseRecord as any)["output_usage_units"],
})
}),
);
}
@@ -464,7 +465,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
key,
pgValue,
chValue: (clickhouseRecord as any)["input_usage_units"],
})
}),
);
}
@@ -481,7 +482,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
key,
pgValue,
chValue: (clickhouseRecord as any)["total_usage_units"],
})
}),
);
}
@@ -499,7 +500,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
key,
pgValue,
chValue: (clickhouseRecord as any)["input_cost"],
})
}),
);
}
@@ -517,7 +518,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
key,
pgValue,
chValue: (clickhouseRecord as any)["output_cost"],
})
}),
);
}
@@ -535,7 +536,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
key,
pgValue,
chValue: (clickhouseRecord as any)["total_cost"],
})
}),
);
}
@@ -546,7 +547,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
if (
pgValue !== null &&
Math.abs(
Number(pgValue) - (clickhouseRecord as any)["provided_input_cost"]
Number(pgValue) - (clickhouseRecord as any)["provided_input_cost"],
) > 1e-9
) {
throw new Error(
@@ -554,7 +555,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
key,
pgValue,
chValue: (clickhouseRecord as any)["provided_input_cost"],
})
}),
);
}
@@ -565,7 +566,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
if (
pgValue !== null &&
Math.abs(
Number(pgValue) - (clickhouseRecord as any)["provided_output_cost"]
Number(pgValue) - (clickhouseRecord as any)["provided_output_cost"],
) > 1e-9
) {
throw new Error(
@@ -573,7 +574,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
key,
pgValue,
chValue: (clickhouseRecord as any)["provided_output_cost"],
})
}),
);
}
@@ -584,7 +585,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
if (
pgValue !== null &&
Math.abs(
Number(pgValue) - (clickhouseRecord as any)["provided_total_cost"]
Number(pgValue) - (clickhouseRecord as any)["provided_total_cost"],
) > 1e-9
) {
throw new Error(
@@ -592,7 +593,7 @@ async function verifyClickhouseObservation(postgresObservation: any) {
key,
pgValue: Number(pgValue),
chValue: (clickhouseRecord as any)["provided_total_cost"],
})
}),
);
}
@@ -616,7 +617,7 @@ async function verifyClickhouseTrace(postgresTrace: any) {
const clickhouseTrace = (await clickhouseResult.json())[0];
if (!clickhouseTrace) {
throw new Error(
`Trace ${traceId} not found in Clickhouse for project ${projectId}`
`Trace ${traceId} not found in Clickhouse for project ${projectId}`,
);
}
@@ -682,7 +683,7 @@ async function verifyClickhouseTrace(postgresTrace: any) {
Object.entries(chValue).map(([k, v]) => [
k,
JSON.parse(v as any),
])
]),
);
if (
@@ -691,11 +692,11 @@ async function verifyClickhouseTrace(postgresTrace: any) {
parsedChMetadata,
typeof parsedChMetadata === "string"
? undefined
: Object.keys(parsedChMetadata as any).sort()
: Object.keys(parsedChMetadata as any).sort(),
)
) {
throw new Error(
getErrorMessage({ key, pgValue, chValue: parsedChMetadata })
getErrorMessage({ key, pgValue, chValue: parsedChMetadata }),
);
}
@@ -721,7 +722,7 @@ async function verifyClickhouseTrace(postgresTrace: any) {
parsedChValue,
parsedChValue instanceof Object
? Object.keys(parsedChValue as any).sort()
: undefined
: undefined,
)
) {
throw new Error(
@@ -729,7 +730,7 @@ async function verifyClickhouseTrace(postgresTrace: any) {
key,
pgValue: JSON.stringify(parsedPgValue),
chValue: JSON.stringify(parsedChValue),
})
}),
);
}
@@ -753,7 +754,7 @@ async function verifyClickhouseScore(postgresScore: any) {
const clickhouseScore = (await clickhouseResult.json())[0];
if (!clickhouseScore) {
throw new Error(
`Score ${scoreId} not found in Clickhouse for project ${projectId}`
`Score ${scoreId} not found in Clickhouse for project ${projectId}`,
);
}
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as serverExports from "@langfuse/shared/src/server";
import { env } from "../../env";
import logger from "../../logger";
import { logger } from "@langfuse/shared/src/server";
import { ClickhouseWriter, TableName } from "../ClickhouseWriter";
// Mock recordHistogram, recordCount, recordGauge
@@ -17,6 +17,11 @@ vi.mock("@langfuse/shared/src/server", async (importOriginal) => {
recordHistogram: vi.fn(),
recordCount: vi.fn(),
recordGauge: vi.fn(),
logger: {
info: vi.fn(),
debug: vi.fn(),
error: vi.fn(),
},
};
});
@@ -32,16 +37,6 @@ vi.mock("../../env", async (importOriginal) => {
};
});
vi.mock("../../logger", () => {
return {
default: {
info: vi.fn(),
debug: vi.fn(),
error: vi.fn(),
},
};
});
describe("ClickhouseWriter", () => {
let writer: ClickhouseWriter;
@@ -69,13 +64,13 @@ describe("ClickhouseWriter", () => {
it("should initialize with correct values", () => {
expect(writer.batchSize).toBe(
env.LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE
env.LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE,
);
expect(writer.writeInterval).toBe(
env.LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS
env.LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS,
);
expect(writer.maxAttempts).toBe(
env.LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS
env.LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS,
);
});
@@ -148,7 +143,7 @@ describe("ClickhouseWriter", () => {
expect(mockInsert).toHaveBeenCalledTimes(writer.maxAttempts);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining("Max attempts reached")
expect.stringContaining("Max attempts reached"),
);
expect(writer["queue"][TableName.Traces]).toHaveLength(0);
});
@@ -164,7 +159,7 @@ describe("ClickhouseWriter", () => {
expect(mockInsert).toHaveBeenCalledTimes(1);
expect(writer["intervalId"]).toBeNull();
expect(logger.info).toHaveBeenCalledWith(
"ClickhouseWriter shutdown complete."
"ClickhouseWriter shutdown complete.",
);
});
@@ -204,7 +199,7 @@ describe("ClickhouseWriter", () => {
expect(setIntervalSpy).toHaveBeenCalledWith(
expect.any(Function),
writer.writeInterval
writer.writeInterval,
);
});
@@ -246,17 +241,17 @@ describe("ClickhouseWriter", () => {
const concurrentWrites = 1000;
const writes = Array.from({ length: concurrentWrites }, (_, i) =>
writer.addToQueue(TableName.Traces, { id: `${i}`, name: `test${i}` })
writer.addToQueue(TableName.Traces, { id: `${i}`, name: `test${i}` }),
);
await Promise.all(writes);
await vi.advanceTimersByTimeAsync(writer.writeInterval);
expect(mockInsert).toHaveBeenCalledTimes(
Math.ceil(concurrentWrites / writer.batchSize)
Math.ceil(concurrentWrites / writer.batchSize),
);
expect(writer["queue"][TableName.Traces].length).toBeLessThan(
writer.batchSize
writer.batchSize,
);
});
@@ -273,13 +268,13 @@ describe("ClickhouseWriter", () => {
expect(metricsDistributionSpy).toHaveBeenCalledWith(
"ingestion_clickhouse_insert_wait_time",
expect.any(Number),
{ unit: "milliseconds" }
{ unit: "milliseconds" },
);
expect(metricsDistributionSpy).toHaveBeenCalledWith(
"ingestion_clickhouse_insert_processing_time",
expect.any(Number),
{ unit: "milliseconds" }
{ unit: "milliseconds" },
);
});
@@ -294,12 +289,12 @@ describe("ClickhouseWriter", () => {
await vi.advanceTimersByTimeAsync(writer.writeInterval);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining("Network error")
expect.stringContaining("Network error"),
);
await vi.advanceTimersByTimeAsync(writer.writeInterval);
expect(logger.error).toHaveBeenCalledWith(
expect.stringContaining("Timeout")
expect.stringContaining("Timeout"),
);
await vi.advanceTimersByTimeAsync(writer.writeInterval);
@@ -322,9 +317,9 @@ describe("ClickhouseWriter", () => {
expect(mockInsert).toHaveBeenCalledWith(
expect.objectContaining({
values: expect.arrayContaining(
new Array(partialQueueSize).fill(expect.any(Object))
new Array(partialQueueSize).fill(expect.any(Object)),
),
})
}),
);
expect(writer["queue"][TableName.Traces]).toHaveLength(0);
});
+10 -10
View File
@@ -8,7 +8,7 @@ import {
} from "@langfuse/shared/src/server";
import { env } from "../../env";
import logger from "../../logger";
import { logger } from "@langfuse/shared/src/server";
import { instrumentAsync } from "@langfuse/shared/src/server";
import { SpanKind } from "@opentelemetry/api";
@@ -48,7 +48,7 @@ export class ClickhouseWriter {
private start() {
logger.info(
`Starting ClickhouseWriter. Max interval: ${this.writeInterval} ms, Max batch size: ${this.batchSize}`
`Starting ClickhouseWriter. Max interval: ${this.writeInterval} ms, Max batch size: ${this.batchSize}`,
);
this.intervalId = setInterval(() => {
@@ -91,7 +91,7 @@ export class ClickhouseWriter {
]).catch((err) => {
logger.error("ClickhouseWriter.flushAll", err);
});
}
},
);
}
@@ -101,7 +101,7 @@ export class ClickhouseWriter {
const queueItems = entityQueue.splice(
0,
fullQueue ? entityQueue.length : this.batchSize
fullQueue ? entityQueue.length : this.batchSize,
);
// Log wait time
@@ -126,11 +126,11 @@ export class ClickhouseWriter {
Date.now() - processingStartTime,
{
unit: "milliseconds",
}
},
);
logger.debug(
`Flushed ${queueItems.length} records to Clickhouse ${tableName}. New queue length: ${entityQueue.length}`
`Flushed ${queueItems.length} records to Clickhouse ${tableName}. New queue length: ${entityQueue.length}`,
);
recordGauge(
@@ -139,7 +139,7 @@ export class ClickhouseWriter {
{
unit: "records",
entityType: tableName,
}
},
);
} catch (err) {
logger.error(`ClickhouseWriter.flush ${tableName}`, err);
@@ -154,7 +154,7 @@ export class ClickhouseWriter {
} else {
// TODO - Add to a dead letter queue in Redis rather than dropping
logger.error(
`Max attempts reached for ${tableName} record. Dropping record ${item.data}.`
`Max attempts reached for ${tableName} record. Dropping record ${item.data}.`,
);
}
});
@@ -163,7 +163,7 @@ export class ClickhouseWriter {
public addToQueue<T extends TableName>(
tableName: T,
data: RecordInsertType<T>
data: RecordInsertType<T>,
) {
const entityQueue = this.queue[tableName];
entityQueue.push({
@@ -200,7 +200,7 @@ export class ClickhouseWriter {
});
logger.debug(
`ClickhouseWriter.writeToClickhouse: ${Date.now() - startTime} ms`
`ClickhouseWriter.writeToClickhouse: ${Date.now() - startTime} ms`,
);
recordGauge("ingestion_clickhouse_insert", params.records.length);
+24 -24
View File
@@ -32,7 +32,7 @@ import {
import { tokenCount } from "../../features/tokenisation/usage";
import { instrumentAsync } from "@langfuse/shared/src/server";
import logger from "../../logger";
import { logger } from "@langfuse/shared/src/server";
import { ClickhouseWriter, TableName } from "../ClickhouseWriter";
import { convertJsonSchemaToRecord, overwriteObject } from "./utils";
@@ -70,7 +70,7 @@ export class IngestionService {
private redis: Redis,
prisma: PrismaClient,
private clickHouseWriter: ClickhouseWriter,
private clickhouseClient: ClickhouseClientType
private clickhouseClient: ClickhouseClientType,
) {
this.promptService = new PromptService(prisma, redis);
}
@@ -80,12 +80,12 @@ export class IngestionService {
const eventList = (await this.redis.lrange(bufferKey, 0, -1))
.map((serializedEventData) => {
const parsed = ingestionEventWithProjectId.safeParse(
JSON.parse(serializedEventData)
JSON.parse(serializedEventData),
);
if (!parsed.success) {
logger.error(
`Failed to parse event ${serializedEventData} : ${parsed.error}`
`Failed to parse event ${serializedEventData} : ${parsed.error}`,
);
return null;
@@ -97,7 +97,7 @@ export class IngestionService {
if (eventList.length === 0) {
throw new Error(
`No valid events found in buffer for flushKey ${flushKey}`
`No valid events found in buffer for flushKey ${flushKey}`,
);
}
@@ -173,7 +173,7 @@ export class IngestionService {
if (!clickhouseScoreRecord && !this.hasCreateEvent(scoreEventList)) {
throw new Error(
`No create event or existing record found for score with id ${entityId} in project ${projectId}`
`No create event or existing record found for score with id ${entityId} in project ${projectId}`,
);
}
@@ -211,7 +211,7 @@ export class IngestionService {
if (!clickhouseTraceRecord && !this.hasCreateEvent(traceEventList)) {
throw new Error(
`No create event or existing record found for trace with id ${entityId} in project ${projectId}`
`No create event or existing record found for trace with id ${entityId} in project ${projectId}`,
);
}
@@ -253,7 +253,7 @@ export class IngestionService {
!this.hasCreateEvent(observationEventList)
) {
throw new Error(
`No create event or existing record found for observation with id ${entityId} in project ${projectId}`
`No create event or existing record found for observation with id ${entityId} in project ${projectId}`,
);
}
@@ -284,7 +284,7 @@ export class IngestionService {
this.clickHouseWriter.addToQueue(
TableName.Observations,
finalObservationRecord
finalObservationRecord,
);
}
@@ -300,7 +300,7 @@ export class IngestionService {
const mergedRecord = this.mergeRecords(
recordsToMerge,
immutableEntityKeys[TableName.Scores]
immutableEntityKeys[TableName.Scores],
);
return scoreRecordInsertSchema.parse(mergedRecord);
@@ -318,7 +318,7 @@ export class IngestionService {
const mergedRecord = this.mergeRecords(
recordsToMerge,
immutableEntityKeys[TableName.Traces]
immutableEntityKeys[TableName.Traces],
);
return traceRecordInsertSchema.parse(mergedRecord);
@@ -338,7 +338,7 @@ export class IngestionService {
const mergedRecord = this.mergeRecords(
recordsToMerge,
immutableEntityKeys[TableName.Observations]
immutableEntityKeys[TableName.Observations],
);
const parsedObservationRecord =
@@ -362,7 +362,7 @@ export class IngestionService {
private mergeRecords<T extends InsertRecord>(
records: T[],
immutableEntityKeys: string[]
immutableEntityKeys: string[],
): unknown {
if (records.length === 0) {
throw new Error("No records to merge");
@@ -398,7 +398,7 @@ export class IngestionService {
private async getPrompt(
projectId: string,
observationEventList: ObservationEvent[]
observationEventList: ObservationEvent[],
): Promise<ObservationPrompt | null> {
const lastObservationWithPromptInfo = observationEventList
.slice()
@@ -419,7 +419,7 @@ export class IngestionService {
}
private hasPromptInformation(
event: ObservationEvent
event: ObservationEvent,
): event is ObservationEvent & {
body: { promptName: string; promptVersion: number };
} {
@@ -461,7 +461,7 @@ export class IngestionService {
const tokenCosts = IngestionService.calculateTokenCosts(
internalModel,
observationRecord,
tokenCounts
tokenCounts,
);
return {
@@ -474,7 +474,7 @@ export class IngestionService {
private getTokenCounts(
observationRecord: ObservationRecordInsertType,
model: Model | null | undefined
model: Model | null | undefined,
): Pick<
ObservationRecordInsertType,
"input_usage_units" | "output_usage_units" | "total_usage_units"
@@ -521,7 +521,7 @@ export class IngestionService {
input_usage_units?: number | null;
output_usage_units?: number | null;
total_usage_units?: number | null;
}
},
): {
input_cost: number | null | undefined;
output_cost: number | null | undefined;
@@ -613,7 +613,7 @@ export class IngestionService {
: table === TableName.Scores
? convertScoreReadToInsert(recordParser[table].parse(result[0]))
: convertObservationReadToInsert(
recordParser[table].parse(result[0])
recordParser[table].parse(result[0]),
);
});
}
@@ -631,7 +631,7 @@ export class IngestionService {
// in the default implementation, we set timestamps server side if not provided.
// we need to insert timestamps here and change the SDKs to send timestamps client side.
timestamp: this.getMillisecondTimestamp(
trace.body.timestamp ?? trace.timestamp
trace.body.timestamp ?? trace.timestamp,
),
name: trace.body.name,
user_id: trace.body.userId,
@@ -714,7 +714,7 @@ export class IngestionService {
type: observationType,
name: obs.body.name,
start_time: this.getMillisecondTimestamp(
obs.body.startTime ?? obs.timestamp
obs.body.startTime ?? obs.timestamp,
),
end_time:
"endTime" in obs.body && obs.body.endTime
@@ -763,7 +763,7 @@ export class IngestionService {
}
private stringify(
obj: string | object | number | boolean | undefined | null
obj: string | object | number | boolean | undefined | null,
): string | undefined {
if (obj == null) return; // return undefined on undefined or null
@@ -775,10 +775,10 @@ export class IngestionService {
}
private hasCreateEvent(
eventList: ObservationEvent[] | ScoreEventType[] | TraceEventType[]
eventList: ObservationEvent[] | ScoreEventType[] | TraceEventType[],
): boolean {
return eventList.some((event) =>
event.type.toLowerCase().includes("create")
event.type.toLowerCase().includes("create"),
);
}
}
+1 -2
View File
@@ -1,5 +1,4 @@
import logger from "../logger";
import { logger } from "@langfuse/shared/src/server";
import { redis } from "@langfuse/shared/src/server";
import { ClickhouseWriter } from "../services/ClickhouseWriter";