Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
898cca56f9 | ||
|
|
83e052e769 | ||
|
|
b5f19771fa | ||
|
|
644390183b | ||
|
|
4d8e9003fe | ||
|
|
651c46a270 | ||
|
|
6b127bced0 | ||
|
|
e3a837b561 |
+15
-2
@@ -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"
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.77.0",
|
||||
"version": "2.78.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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";
|
||||
|
||||
+18
-17
@@ -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 === ">="
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
+8
-7
@@ -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") : ""}`,
|
||||
);
|
||||
}
|
||||
+5
-5
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
+4
-4
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
+7
-6
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
Generated
+391
-491
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -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 @@
|
||||
export const VERSION = "v2.77.0";
|
||||
export const VERSION = "v2.78.0";
|
||||
|
||||
@@ -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
@@ -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.",
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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", ""),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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", ""),
|
||||
});
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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,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
@@ -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 @@
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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
@@ -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}`);
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
@@ -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,7 +1,7 @@
|
||||
import { Processor, Queue } from "bullmq";
|
||||
import logger from "../logger";
|
||||
import {
|
||||
redis,
|
||||
logger,
|
||||
QueueName,
|
||||
QueueJobs,
|
||||
instrumentAsync,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
Reference in New Issue
Block a user