Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
898cca56f9 | ||
|
|
83e052e769 | ||
|
|
b5f19771fa | ||
|
|
644390183b | ||
|
|
4d8e9003fe | ||
|
|
651c46a270 | ||
|
|
6b127bced0 | ||
|
|
e3a837b561 | ||
|
|
11c04f02c8 | ||
|
|
536aa138bc | ||
|
|
61b596c46a | ||
|
|
ddf43c0836 | ||
|
|
d5ab03892a | ||
|
|
918b6643d2 | ||
|
|
937b4c0f0f |
+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"
|
||||
|
||||
@@ -40,6 +40,9 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set NEXT_PUBLIC_BUILD_ID
|
||||
run: echo "NEXT_PUBLIC_BUILD_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and run both images from compose
|
||||
run: |
|
||||
docker compose -f docker-compose.build.yml up -d
|
||||
@@ -107,7 +110,6 @@ jobs:
|
||||
- name: Seed DB
|
||||
run: |
|
||||
pnpm run db:migrate
|
||||
pnpm run db:seed
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
@@ -118,6 +120,16 @@ jobs:
|
||||
|
||||
- name: Start Langfuse
|
||||
run: (pnpm run start&)
|
||||
env:
|
||||
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
|
||||
@@ -321,6 +333,9 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set NEXT_PUBLIC_BUILD_ID
|
||||
run: echo "NEXT_PUBLIC_BUILD_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to the GitHub Container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
|
||||
@@ -27,3 +27,4 @@ jobs:
|
||||
PORTER_STACK_NAME: web
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4060 }}
|
||||
PORTER_NEXT_PUBLIC_BUILD_ID: ${{ steps.vars.outputs.sha_short }}
|
||||
|
||||
@@ -27,3 +27,4 @@ jobs:
|
||||
PORTER_STACK_NAME: web
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4037 }}
|
||||
PORTER_NEXT_PUBLIC_BUILD_ID: ${{ steps.vars.outputs.sha_short }}
|
||||
|
||||
@@ -27,3 +27,4 @@ jobs:
|
||||
PORTER_STACK_NAME: web
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4054 }}
|
||||
PORTER_NEXT_PUBLIC_BUILD_ID: ${{ steps.vars.outputs.sha_short }}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.76.0",
|
||||
"version": "2.78.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
"import": "./dist/src/server/index.js",
|
||||
"require": "./dist/src/server/index.js"
|
||||
},
|
||||
"./src/server/auth/apiKeys": {
|
||||
"import": "./dist/src/server/auth/apiKeys.js",
|
||||
"require": "./dist/src/server/auth/apiKeys.js"
|
||||
},
|
||||
"./encryption": {
|
||||
"import": "./dist/src/encryption/index.js",
|
||||
"require": "./dist/src/encryption/index.js"
|
||||
@@ -75,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"
|
||||
},
|
||||
@@ -103,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"),
|
||||
@@ -38,6 +38,11 @@ const EnvSchema = z.object({
|
||||
.number()
|
||||
.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,15 +1,12 @@
|
||||
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/auth";
|
||||
export * from "./server/auth/apiKeys";
|
||||
export * from "./observationsTable";
|
||||
export * from "./utils/zod";
|
||||
export * from "./utils/json";
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { compare, hash } from "bcryptjs";
|
||||
import { randomUUID } from "crypto";
|
||||
import * as crypto from "crypto";
|
||||
import { env } from "../../env";
|
||||
|
||||
export function getDisplaySecretKey(secretKey: string) {
|
||||
return secretKey.slice(0, 6) + "..." + secretKey.slice(-4);
|
||||
}
|
||||
|
||||
export async function hashSecretKey(key: string) {
|
||||
// legacy, uses bcrypt, transformed into hashed key upon first use
|
||||
const hashedKey = await hash(key, 11);
|
||||
return hashedKey;
|
||||
}
|
||||
|
||||
async function generateKeySet() {
|
||||
return {
|
||||
pk: `pk-lf-${randomUUID()}`,
|
||||
sk: `sk-lf-${randomUUID()}`,
|
||||
};
|
||||
}
|
||||
|
||||
export async function verifySecretKey(key: string, hashedKey: string) {
|
||||
const isValid = await compare(key, hashedKey);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
export function createShaHash(privateKey: string, salt: string): string {
|
||||
const hash = crypto
|
||||
.createHash("sha256")
|
||||
.update(privateKey)
|
||||
.update(crypto.createHash("sha256").update(salt, "utf8").digest("hex"))
|
||||
.digest("hex");
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
export async function createAndAddApiKeysToDb(p: {
|
||||
prisma: PrismaClient;
|
||||
projectId: string;
|
||||
note?: string;
|
||||
predefinedKeys?: {
|
||||
secretKey: string;
|
||||
publicKey: string;
|
||||
};
|
||||
}) {
|
||||
const salt = env.SALT;
|
||||
if (!salt) {
|
||||
throw new Error("SALT is not set");
|
||||
}
|
||||
|
||||
const { pk, sk } = p.predefinedKeys
|
||||
? { pk: p.predefinedKeys.publicKey, sk: p.predefinedKeys.secretKey }
|
||||
: await generateKeySet();
|
||||
|
||||
const hashedSk = await hashSecretKey(sk);
|
||||
const displaySk = getDisplaySecretKey(sk);
|
||||
|
||||
const hashFromProvidedKey = createShaHash(sk, salt);
|
||||
|
||||
const apiKey = await p.prisma.apiKey.create({
|
||||
data: {
|
||||
projectId: p.projectId,
|
||||
publicKey: pk,
|
||||
hashedSecretKey: hashedSk,
|
||||
displaySecretKey: displaySk,
|
||||
fastHashedSecretKey: hashFromProvidedKey,
|
||||
note: p.note,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: apiKey.id,
|
||||
createdAt: apiKey.createdAt,
|
||||
note: apiKey.note,
|
||||
publicKey: apiKey.publicKey,
|
||||
secretKey: sk,
|
||||
displaySecretKey: displaySk,
|
||||
};
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { compare, hash } from "bcryptjs";
|
||||
import { randomUUID } from "crypto";
|
||||
import * as crypto from "crypto";
|
||||
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
|
||||
|
||||
export function generateSecretKey() {
|
||||
return `sk-lf-${randomUUID()}`;
|
||||
}
|
||||
|
||||
export function generatePublicKey() {
|
||||
return `pk-lf-${randomUUID()}`;
|
||||
}
|
||||
|
||||
export function getDisplaySecretKey(secretKey: string) {
|
||||
return secretKey.slice(0, 6) + "..." + secretKey.slice(-4);
|
||||
}
|
||||
|
||||
export async function hashSecretKey(key: string) {
|
||||
// legacy, uses bcrypt, transformed into hashed key upon first use
|
||||
const hashedKey = await hash(key, 11);
|
||||
return hashedKey;
|
||||
}
|
||||
|
||||
export async function generateKeySet() {
|
||||
const pk = generatePublicKey();
|
||||
const sk = generateSecretKey();
|
||||
const hashedSk = await hashSecretKey(sk);
|
||||
const displaySk = getDisplaySecretKey(sk);
|
||||
|
||||
return {
|
||||
pk,
|
||||
sk,
|
||||
hashedSk,
|
||||
displaySk,
|
||||
};
|
||||
}
|
||||
|
||||
export async function verifySecretKey(key: string, hashedKey: string) {
|
||||
const isValid = await compare(key, hashedKey);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
export function createShaHash(privateKey: string, salt: string): string {
|
||||
const hash = crypto
|
||||
.createHash("sha256")
|
||||
.update(privateKey)
|
||||
.update(crypto.createHash("sha256").update(salt, "utf8").digest("hex"))
|
||||
.digest("hex");
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
export interface CustomSSOUser extends Record<string, any> {
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
export function CustomSSOProvider<P extends CustomSSOUser>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "custom",
|
||||
name: "CustomSSOProvider",
|
||||
type: "oauth",
|
||||
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
|
||||
authorization: { params: { scope: "openid email profile" } }, // overridden by options.authorization to be able to set custom scopes, deep merged with this default
|
||||
checks: ["pkce", "state"],
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
options,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
|
||||
|
||||
interface CustomSSOUser extends Record<string, any> {
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
export function CustomSSOProvider<P extends CustomSSOUser>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "custom",
|
||||
name: "CustomSSOProvider",
|
||||
type: "oauth",
|
||||
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
|
||||
authorization: { params: { scope: "openid email profile" } }, // overridden by options.authorization to be able to set custom scopes, deep merged with this default
|
||||
checks: ["pkce", "state"],
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
options,
|
||||
};
|
||||
}
|
||||
+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 === ">="
|
||||
@@ -3,7 +3,8 @@ export * from "./services/email/organizationInvitation/sendMembershipInvitationE
|
||||
export * from "./services/email/batchExportSuccess/sendBatchExportSuccessEmail";
|
||||
export * from "./services/email/passwordReset/sendResetPasswordVerificationRequest";
|
||||
export * from "./services/PromptService";
|
||||
export * from "./auth/auth";
|
||||
export * from "./auth/apiKeys";
|
||||
export * from "./auth/customSsoProvider";
|
||||
export * from "./llm/fetchLLMCompletion";
|
||||
export * from "./llm/types";
|
||||
export * from "./utils/DatabaseReadStream";
|
||||
@@ -24,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
+6
-1
@@ -44,6 +44,11 @@ ARG NEXT_PUBLIC_CRISP_WEBSITE_ID
|
||||
ARG NEXT_PUBLIC_SENTRY_DSN
|
||||
ARG NEXT_LANGFUSE_TRACING_SAMPLE_RATE
|
||||
|
||||
# Accept build id as NEXT_PUBLIC_BUILD_ID or PORTER_NEXT_PUBLIC_BUILD_ID
|
||||
ARG PORTER_NEXT_PUBLIC_BUILD_ID
|
||||
ARG NEXT_PUBLIC_BUILD_ID
|
||||
ENV NEXT_PUBLIC_BUILD_ID=${PORTER_NEXT_PUBLIC_BUILD_ID:-$NEXT_PUBLIC_BUILD_ID}
|
||||
|
||||
# Copy source code of isolated subworkspace
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
|
||||
@@ -56,7 +61,7 @@ RUN rm -f ./web/src/middleware.ts
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV NEXT_MANUAL_SIG_HANDLE true
|
||||
|
||||
RUN turbo run build --filter=web
|
||||
RUN NODE_OPTIONS='--max-old-space-size=4096' turbo run build --filter=web
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM --platform=${TARGETPLATFORM:-linux/amd64} base AS runner
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.76.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,7 +1,4 @@
|
||||
import { hash } from "bcryptjs";
|
||||
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { getDisplaySecretKey, hashSecretKey } from "@langfuse/shared";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { type z } from "zod";
|
||||
|
||||
@@ -24,10 +21,7 @@ export const pruneDatabase = async () => {
|
||||
await prisma.llmApiKeys.deleteMany();
|
||||
};
|
||||
|
||||
export function createBasicAuthHeader(
|
||||
username: string,
|
||||
password: string,
|
||||
): string {
|
||||
function createBasicAuthHeader(username: string, password: string): string {
|
||||
const base64Credentials = Buffer.from(`${username}:${password}`).toString(
|
||||
"base64",
|
||||
);
|
||||
@@ -97,38 +91,3 @@ export async function makeZodVerifiedAPICall<T extends z.ZodTypeAny>(
|
||||
}
|
||||
return { body: resBody, status };
|
||||
}
|
||||
|
||||
export const setupUserAndProject = async () => {
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
id: "user-1",
|
||||
name: "Demo User",
|
||||
email: "demo@langfuse.com",
|
||||
password: await hash("password", 12),
|
||||
},
|
||||
});
|
||||
|
||||
const project = await prisma.project.create({
|
||||
data: {
|
||||
id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
name: "llm-app",
|
||||
apiKeys: {
|
||||
create: [
|
||||
{
|
||||
note: "seeded key",
|
||||
hashedSecretKey: await hashSecretKey("sk-lf-1234567890"),
|
||||
displaySecretKey: getDisplaySecretKey("sk-lf-1234567890"),
|
||||
publicKey: "pk-lf-1234567890",
|
||||
},
|
||||
],
|
||||
},
|
||||
projectMembers: {
|
||||
create: {
|
||||
role: "OWNER",
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return { user, project };
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
BadgeCheck,
|
||||
Github,
|
||||
HardDriveDownload,
|
||||
Info,
|
||||
Map,
|
||||
Newspaper,
|
||||
} from "lucide-react";
|
||||
@@ -20,6 +21,7 @@ import { api } from "@/src/utils/api";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { useIsEeEnabled } from "@/src/ee/utils/useIsEeEnabled";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
export const VersionLabel = ({ className }: { className?: string }) => {
|
||||
const checkUpdate = api.public.checkUpdate.useQuery(undefined, {
|
||||
@@ -30,6 +32,7 @@ export const VersionLabel = ({ className }: { className?: string }) => {
|
||||
onError: (error) => console.error("checkUpdate error", error), // do not render default error message
|
||||
});
|
||||
const isEeVersion = useIsEeEnabled();
|
||||
const isLangfuseCloud = Boolean(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION);
|
||||
|
||||
const hasUpdate =
|
||||
!env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION &&
|
||||
@@ -46,8 +49,9 @@ export const VersionLabel = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="xs" className={className}>
|
||||
<Button variant="ghost" size="xs" className={cn("text-xs",className)}>
|
||||
{VERSION}
|
||||
{!isLangfuseCloud && (isEeVersion ? " EE" : " OSS")}
|
||||
{hasUpdate && <ArrowUp className={`ml-1 h-3 w-3 ${color}`} />}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -96,6 +100,17 @@ export const VersionLabel = ({ className }: { className?: string }) => {
|
||||
Roadmap
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{!isLangfuseCloud && !isEeVersion && (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href="https://langfuse.com/docs/deployment/feature-overview"
|
||||
target="_blank"
|
||||
>
|
||||
<Info size={16} className="mr-2" />
|
||||
Compare Versions
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{hasUpdate && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
@@ -39,7 +39,7 @@ export const ROUTES: Route[] = [
|
||||
name: "Langfuse",
|
||||
pathname: "/",
|
||||
icon: LangfuseIcon,
|
||||
label: <VersionLabel />,
|
||||
label: <VersionLabel className="-ml-3" />,
|
||||
},
|
||||
{
|
||||
name: "Projects",
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.76.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(),
|
||||
|
||||
@@ -171,6 +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_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(),
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -194,6 +203,7 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
|
||||
NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(),
|
||||
NEXT_PUBLIC_CRISP_WEBSITE_ID: z.string().optional(),
|
||||
NEXT_PUBLIC_BUILD_ID: z.string().optional(),
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -206,6 +216,7 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_DEMO_ORG_ID: process.env.NEXT_PUBLIC_DEMO_ORG_ID,
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
NEXT_PUBLIC_BUILD_ID: process.env.NEXT_PUBLIC_BUILD_ID,
|
||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
||||
NEXTAUTH_COOKIE_DOMAIN: process.env.NEXTAUTH_COOKIE_DOMAIN,
|
||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
|
||||
@@ -321,6 +332,16 @@ export const env = createEnv({
|
||||
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||
SENTRY_CSP_REPORT_URI: process.env.SENTRY_CSP_REPORT_URI,
|
||||
LANGFUSE_RATE_LIMITS_ENABLED: process.env.LANGFUSE_RATE_LIMITS_ENABLED,
|
||||
// provisioning
|
||||
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
|
||||
|
||||
@@ -8,6 +8,7 @@ export type AuditableResource =
|
||||
| "comment"
|
||||
| "datasetItem"
|
||||
| "dataset"
|
||||
| "datasetRun"
|
||||
| "trace"
|
||||
| "project"
|
||||
| "observation"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -52,8 +53,8 @@ export async function signupApiHandler(
|
||||
}
|
||||
|
||||
// EE: check if custom SSO configuration is enabled for this domain
|
||||
const customSsoProvider = await getSsoAuthProviderIdForDomain(domain);
|
||||
if (customSsoProvider) {
|
||||
const multiTenantSsoProvider = await getSsoAuthProviderIdForDomain(domain);
|
||||
if (multiTenantSsoProvider) {
|
||||
res.status(422).json({
|
||||
message: "You must sign in via SSO for this domain.",
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,12 +20,23 @@ import {
|
||||
} from "@/src/features/scores/components/ScoreDetailColumnHelpers";
|
||||
import { type ScoreAggregate } from "@/src/features/scores/lib/types";
|
||||
import { useIndividualScoreColumns } from "@/src/features/scores/hooks/useIndividualScoreColumns";
|
||||
import { MoreVertical } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/src/components/ui/dropdown-menu";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { DeleteDatasetRunButton } from "@/src/features/datasets/components/DeleteDatasetRunButton";
|
||||
|
||||
type DatasetRunRowKey = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type DatasetRunRowData = {
|
||||
key: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
key: DatasetRunRowKey;
|
||||
createdAt: string;
|
||||
countRunItems: string;
|
||||
avgLatency: number;
|
||||
@@ -45,6 +56,7 @@ export function DatasetRunsTable(props: {
|
||||
pageIndex: withDefault(NumberParam, 0),
|
||||
pageSize: withDefault(NumberParam, 50),
|
||||
});
|
||||
|
||||
const [rowHeight, setRowHeight] = useRowHeightLocalStorage(
|
||||
"datasetRuns",
|
||||
"s",
|
||||
@@ -149,6 +161,35 @@ export function DatasetRunsTable(props: {
|
||||
) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "actions",
|
||||
header: "Actions",
|
||||
size: 70,
|
||||
cell: ({ row }) => {
|
||||
const key: DatasetRunRowKey = row.getValue("key");
|
||||
const { id: datasetRunId } = key;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only [position:relative]">Open menu</span>
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DeleteDatasetRunButton
|
||||
projectId={props.projectId}
|
||||
datasetRunId={datasetRunId}
|
||||
fullWidth
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const convertToTableRow = (
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Trash } from "lucide-react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/src/components/ui/dialog";
|
||||
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { api } from "@/src/utils/api";
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const DeleteDatasetRunButton = ({
|
||||
projectId,
|
||||
datasetRunId,
|
||||
fullWidth = false,
|
||||
redirectUrl,
|
||||
}: {
|
||||
projectId: string;
|
||||
datasetRunId: string;
|
||||
fullWidth?: boolean;
|
||||
redirectUrl?: string;
|
||||
}) => {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const capture = usePostHogClientCapture();
|
||||
const hasAccess = useHasProjectAccess({
|
||||
projectId: projectId,
|
||||
scope: "datasets:CUD",
|
||||
});
|
||||
const utils = api.useUtils();
|
||||
const router = useRouter();
|
||||
const mutDelete = api.datasets.deleteDatasetRun.useMutation({
|
||||
onSuccess: () => {
|
||||
redirectUrl ? router.push(redirectUrl) : utils.datasets.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const button = fullWidth ? (
|
||||
<Button variant="ghost" className="w-full" disabled={!hasAccess}>
|
||||
<div className="flex w-full flex-row items-center gap-1">
|
||||
<Trash className="h-4 w-4" />
|
||||
<span className="text-sm font-normal">Delete</span>
|
||||
</div>
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" size="icon" disabled={!hasAccess}>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
return hasAccess ? (
|
||||
<Dialog
|
||||
open={isDialogOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!mutDelete.isLoading) {
|
||||
setIsDialogOpen(isOpen);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>{button}</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="mb-4">Please confirm</DialogTitle>
|
||||
<DialogDescription className="text-md p-0">
|
||||
This action cannot be undone and removes all the data associated
|
||||
with this dataset run.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Button
|
||||
variant="destructive"
|
||||
loading={mutDelete.isLoading}
|
||||
disabled={mutDelete.isLoading}
|
||||
onClick={async (event) => {
|
||||
event.preventDefault();
|
||||
capture("dataset_run:delete_form_open");
|
||||
await mutDelete.mutateAsync({
|
||||
projectId,
|
||||
datasetRunId,
|
||||
});
|
||||
setIsDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
Delete Dataset Run
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : (
|
||||
button
|
||||
);
|
||||
};
|
||||
@@ -801,4 +801,35 @@ export const datasetRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
}),
|
||||
deleteDatasetRun: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
datasetRunId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
throwIfNoProjectAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "datasets:CUD",
|
||||
});
|
||||
|
||||
const deletedDatasetRun = await ctx.prisma.datasetRuns.delete({
|
||||
where: {
|
||||
id_projectId: {
|
||||
id: input.datasetRunId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
},
|
||||
});
|
||||
await auditLog({
|
||||
session: ctx.session,
|
||||
resourceType: "datasetRun",
|
||||
resourceId: deletedDatasetRun.id,
|
||||
action: "delete",
|
||||
before: deletedDatasetRun,
|
||||
});
|
||||
return deletedDatasetRun;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const showVersionUpdateToast = () => {
|
||||
toast.custom(
|
||||
() => (
|
||||
<div className="flex justify-between">
|
||||
<div className="flex min-w-[300px] flex-1 flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="m-0 text-sm font-medium leading-tight text-foreground/70">
|
||||
We have released a new version of Langfuse. Please refresh your
|
||||
browser to get the latest update.
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size={"sm"}
|
||||
className="text-foreground/50"
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
Refresh page
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
duration: Infinity,
|
||||
style: {
|
||||
padding: "1rem",
|
||||
borderRadius: "0.5rem",
|
||||
border: "1px solid hsl(var(--border))",
|
||||
backgroundColor: "hsl(var(--border))",
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -95,6 +95,7 @@ const events = {
|
||||
"new_from_trace_form_submit",
|
||||
"new_from_trace_form_open",
|
||||
],
|
||||
dataset_run: ["delete_form_open"],
|
||||
notification: ["click_link", "dismiss_notification"],
|
||||
tag: [
|
||||
"add_existing_tag",
|
||||
|
||||
@@ -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,4 @@
|
||||
import { auditLog } from "@/src/features/audit-logs/auditLog";
|
||||
import { createShaHash, generateKeySet } from "@langfuse/shared/src/server";
|
||||
import { throwIfNoProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
@@ -8,7 +7,7 @@ import {
|
||||
import * as z from "zod";
|
||||
import { ApiAuthService } from "@/src/features/public-api/server/apiAuth";
|
||||
import { redis } from "@langfuse/shared/src/server";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { createAndAddApiKeysToDb } from "@langfuse/shared/src/server/auth/apiKeys";
|
||||
|
||||
export const apiKeysRouter = createTRPCRouter({
|
||||
byProjectId: protectedProjectProcedure
|
||||
@@ -56,37 +55,20 @@ export const apiKeysRouter = createTRPCRouter({
|
||||
scope: "apiKeys:create",
|
||||
});
|
||||
|
||||
const { pk, sk, hashedSk, displaySk } = await generateKeySet();
|
||||
|
||||
const salt = env.SALT;
|
||||
const hashFromProvidedKey = createShaHash(sk, salt);
|
||||
|
||||
const apiKey = await ctx.prisma.apiKey.create({
|
||||
data: {
|
||||
projectId: input.projectId,
|
||||
publicKey: pk,
|
||||
hashedSecretKey: hashedSk,
|
||||
displaySecretKey: displaySk,
|
||||
fastHashedSecretKey: hashFromProvidedKey,
|
||||
note: input.note,
|
||||
},
|
||||
const apiKeyMeta = await createAndAddApiKeysToDb({
|
||||
prisma: ctx.prisma,
|
||||
projectId: input.projectId,
|
||||
note: input.note,
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
session: ctx.session,
|
||||
resourceType: "apiKey",
|
||||
resourceId: apiKey.id,
|
||||
resourceId: apiKeyMeta.id,
|
||||
action: "create",
|
||||
});
|
||||
|
||||
return {
|
||||
id: apiKey.id,
|
||||
createdAt: apiKey.createdAt,
|
||||
note: input.note,
|
||||
publicKey: apiKey.publicKey,
|
||||
secretKey: sk,
|
||||
displaySecretKey: displaySk,
|
||||
};
|
||||
return apiKeyMeta;
|
||||
}),
|
||||
delete: protectedProjectProcedure
|
||||
.input(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { createUserEmailPassword } from "@/src/features/auth-credentials/lib/credentialsServerUtils";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { createAndAddApiKeysToDb } from "@langfuse/shared/src/server/auth/apiKeys";
|
||||
|
||||
// Create Organization
|
||||
if (env.LANGFUSE_INIT_ORG_ID) {
|
||||
const org = await prisma.organization.upsert({
|
||||
where: { id: env.LANGFUSE_INIT_ORG_ID },
|
||||
update: {},
|
||||
create: {
|
||||
id: env.LANGFUSE_INIT_ORG_ID,
|
||||
name: env.LANGFUSE_INIT_ORG_NAME ?? "Provisioned Org",
|
||||
},
|
||||
});
|
||||
|
||||
// Create Project: Org -> Project
|
||||
if (env.LANGFUSE_INIT_PROJECT_ID) {
|
||||
await prisma.project.upsert({
|
||||
where: { id: env.LANGFUSE_INIT_PROJECT_ID },
|
||||
update: {},
|
||||
create: {
|
||||
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_INIT_PROJECT_SECRET_KEY &&
|
||||
env.LANGFUSE_INIT_PROJECT_PUBLIC_KEY
|
||||
) {
|
||||
const existingApiKey = await prisma.apiKey.findUnique({
|
||||
where: { publicKey: env.LANGFUSE_INIT_PROJECT_PUBLIC_KEY },
|
||||
});
|
||||
|
||||
// Delete key if project changed
|
||||
if (
|
||||
existingApiKey &&
|
||||
existingApiKey.projectId !== env.LANGFUSE_INIT_PROJECT_ID
|
||||
) {
|
||||
await prisma.apiKey.delete({
|
||||
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_INIT_PROJECT_ID
|
||||
) {
|
||||
await createAndAddApiKeysToDb({
|
||||
prisma,
|
||||
projectId: env.LANGFUSE_INIT_PROJECT_ID,
|
||||
note: "Provisioned API Key",
|
||||
predefinedKeys: {
|
||||
secretKey: env.LANGFUSE_INIT_PROJECT_SECRET_KEY,
|
||||
publicKey: env.LANGFUSE_INIT_PROJECT_PUBLIC_KEY,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create User: Org -> User
|
||||
if (env.LANGFUSE_INIT_USER_EMAIL && env.LANGFUSE_INIT_USER_PASSWORD) {
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: env.LANGFUSE_INIT_USER_EMAIL },
|
||||
});
|
||||
|
||||
let userId = existingUser?.id;
|
||||
|
||||
// Create user if it doesn't exist yet
|
||||
if (!userId) {
|
||||
userId = await createUserEmailPassword(
|
||||
env.LANGFUSE_INIT_USER_EMAIL,
|
||||
env.LANGFUSE_INIT_USER_PASSWORD,
|
||||
env.LANGFUSE_INIT_USER_NAME ?? "Provisioned User",
|
||||
);
|
||||
}
|
||||
|
||||
// Create OrgMembership: Org -> OrgMembership <- User
|
||||
await prisma.organizationMembership.upsert({
|
||||
where: {
|
||||
orgId_userId: { userId, orgId: org.id },
|
||||
},
|
||||
update: { role: "OWNER" },
|
||||
create: {
|
||||
userId,
|
||||
orgId: org.id,
|
||||
role: "OWNER",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,6 @@
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||
await import("./datadog.server.config");
|
||||
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"],
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createNextApiHandler } from "@trpc/server/adapters/next";
|
||||
import { createTRPCContext } from "@/src/server/api/trpc";
|
||||
import { appRouter } from "@/src/server/api/root";
|
||||
import { traceException } from "@langfuse/shared/src/server";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { logger, traceException } from "@langfuse/shared/src/server";
|
||||
|
||||
export const config = {
|
||||
maxDuration: 240,
|
||||
@@ -12,7 +13,17 @@ 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() {
|
||||
return {
|
||||
headers: {
|
||||
"x-build-id": env.NEXT_PUBLIC_BUILD_ID,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { FullScreenPage } from "@/src/components/layouts/full-screen-page";
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { JSONView } from "@/src/components/ui/CodeJsonViewer";
|
||||
import { DatasetRunItemsTable } from "@/src/features/datasets/components/DatasetRunItemsTable";
|
||||
import { DeleteDatasetRunButton } from "@/src/features/datasets/components/DeleteDatasetRunButton";
|
||||
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -36,13 +37,20 @@ export default function Dataset() {
|
||||
{ name: run.data?.name ?? "" },
|
||||
]}
|
||||
actionButtons={
|
||||
<DetailPageNav
|
||||
currentId={runId}
|
||||
path={(id) =>
|
||||
`/project/${projectId}/datasets/${datasetId}/runs/${id}`
|
||||
}
|
||||
listKey="datasetRuns"
|
||||
/>
|
||||
<>
|
||||
<DeleteDatasetRunButton
|
||||
projectId={projectId}
|
||||
datasetRunId={runId}
|
||||
redirectUrl={`/project/${projectId}/datasets/${datasetId}`}
|
||||
/>
|
||||
<DetailPageNav
|
||||
currentId={runId}
|
||||
path={(id) =>
|
||||
`/project/${projectId}/datasets/${datasetId}/runs/${id}`
|
||||
}
|
||||
listKey="datasetRuns"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
@@ -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;
|
||||
|
||||
+13
-7
@@ -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";
|
||||
@@ -104,8 +105,9 @@ const staticProviders: Provider[] = [
|
||||
}
|
||||
|
||||
// EE: Check custom SSO enforcement
|
||||
const customSsoProvider = await getSsoAuthProviderIdForDomain(domain);
|
||||
if (customSsoProvider) {
|
||||
const multiTenantSsoProvider =
|
||||
await getSsoAuthProviderIdForDomain(domain);
|
||||
if (multiTenantSsoProvider) {
|
||||
throw new Error(`You must sign in via SSO for this domain.`);
|
||||
}
|
||||
|
||||
@@ -291,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];
|
||||
@@ -405,19 +407,23 @@ 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");
|
||||
}
|
||||
|
||||
// EE: Check custom SSO enforcement, enforce the specific SSO provider on email domain
|
||||
// This also blocks setting a password for an email that is enforced to use SSO via password reset flow
|
||||
const domain = email.split("@")[1];
|
||||
const customSsoProvider = await getSsoAuthProviderIdForDomain(domain);
|
||||
if (customSsoProvider && account?.provider !== customSsoProvider) {
|
||||
const multiTenantSsoProvider =
|
||||
await getSsoAuthProviderIdForDomain(domain);
|
||||
if (
|
||||
multiTenantSsoProvider &&
|
||||
account?.provider !== multiTenantSsoProvider
|
||||
) {
|
||||
console.log(
|
||||
"Custom SSO provider enforced for domain, user signed in with other provider",
|
||||
);
|
||||
|
||||
+59
-2
@@ -10,7 +10,10 @@ import {
|
||||
httpLink,
|
||||
loggerLink,
|
||||
splitLink,
|
||||
TRPCClientError,
|
||||
type TRPCLink,
|
||||
} from "@trpc/client";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { createTRPCNext } from "@trpc/next";
|
||||
import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
@@ -18,6 +21,7 @@ import superjson from "superjson";
|
||||
import { type AppRouter } from "@/src/server/api/root";
|
||||
import { setUpSuperjson } from "@/src/utils/superjson";
|
||||
import { trpcErrorToast } from "@/src/utils/trpcErrorToast";
|
||||
import { showVersionUpdateToast } from "@/src/features/notifications/showVersionUpdateToast";
|
||||
|
||||
setUpSuperjson();
|
||||
|
||||
@@ -27,6 +31,58 @@ const getBaseUrl = () => {
|
||||
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
|
||||
};
|
||||
|
||||
// global build id used to compare versions to show refresh toast on stale cache hit serving deprecated files
|
||||
let buildId: string | null = null;
|
||||
|
||||
const CLIENT_STALE_CACHE_CODES = [404, 400];
|
||||
|
||||
const handleTrpcError = (error: unknown) => {
|
||||
if (error instanceof TRPCClientError) {
|
||||
const httpStatus: number =
|
||||
typeof error.data?.httpStatus === "number" ? error.data.httpStatus : 500;
|
||||
|
||||
if (CLIENT_STALE_CACHE_CODES.includes(httpStatus)) {
|
||||
if (
|
||||
!!buildId &&
|
||||
!!process.env.NEXT_PUBLIC_BUILD_ID &&
|
||||
buildId !== process.env.NEXT_PUBLIC_BUILD_ID
|
||||
) {
|
||||
showVersionUpdateToast();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trpcErrorToast(error);
|
||||
};
|
||||
|
||||
// onError update build id to compare versions
|
||||
const buildIdLink = (): TRPCLink<AppRouter> => () => {
|
||||
return ({ next, op }) => {
|
||||
return observable((observer) => {
|
||||
const unsubscribe = next(op).subscribe({
|
||||
next(value) {
|
||||
observer.next(value);
|
||||
},
|
||||
error(err) {
|
||||
if (
|
||||
err.meta &&
|
||||
err.meta.response &&
|
||||
err.meta.response instanceof Response
|
||||
) {
|
||||
buildId = err.meta.response.headers.get("x-build-id");
|
||||
}
|
||||
observer.error(err);
|
||||
},
|
||||
complete() {
|
||||
observer.complete();
|
||||
},
|
||||
});
|
||||
return unsubscribe;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/** A set of type-safe react-query hooks for your tRPC API. */
|
||||
export const api = createTRPCNext<AppRouter>({
|
||||
config() {
|
||||
@@ -44,6 +100,7 @@ export const api = createTRPCNext<AppRouter>({
|
||||
* @see https://trpc.io/docs/links
|
||||
*/
|
||||
links: [
|
||||
buildIdLink(),
|
||||
loggerLink({
|
||||
enabled: (opts) =>
|
||||
process.env.NODE_ENV === "development" ||
|
||||
@@ -73,12 +130,12 @@ export const api = createTRPCNext<AppRouter>({
|
||||
queryClientConfig: {
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
onError: (error) => trpcErrorToast(error),
|
||||
onError: (error) => handleTrpcError(error),
|
||||
// react query defaults to `online`, but we want to disable it as it caused issues for some users
|
||||
networkMode: "always",
|
||||
},
|
||||
mutations: {
|
||||
onError: (error) => trpcErrorToast(error),
|
||||
onError: (error) => handleTrpcError(error),
|
||||
// react query defaults to `online`, but we want to disable it as it caused issues for some users
|
||||
networkMode: "always",
|
||||
},
|
||||
|
||||
+2
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "worker",
|
||||
"version": "2.76.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,30 +1,19 @@
|
||||
import { expect, test, describe, vi } from "vitest";
|
||||
import { expect, test, describe, vi, afterEach } from "vitest";
|
||||
import { randomUUID } from "crypto";
|
||||
import { z } from "zod";
|
||||
import logger from "../logger";
|
||||
import { evalJobCreator } from "../queues/evalQueue";
|
||||
import {
|
||||
getTraceUpsertQueue,
|
||||
QueueJobs,
|
||||
TraceUpsertEventSchema,
|
||||
QueueName,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { WorkerManager } from "../queues/workerManager";
|
||||
|
||||
describe.sequential("handle redis events", () => {
|
||||
test("handle redis job succeeding", async () => {
|
||||
vi.mock("../eval-service", () => ({
|
||||
createEvalJobs: async ({
|
||||
data,
|
||||
}: {
|
||||
data: z.infer<typeof TraceUpsertEventSchema>;
|
||||
}) => {
|
||||
return true;
|
||||
},
|
||||
}));
|
||||
afterEach(async () => {
|
||||
await WorkerManager.closeWorkers();
|
||||
});
|
||||
|
||||
// this activates the consumer
|
||||
evalJobCreator?.on("completed", (job, err) => {
|
||||
logger.info(`Eval Job with id ${job?.id} completed`);
|
||||
});
|
||||
test("handle redis job succeeding", async () => {
|
||||
WorkerManager.register(QueueName.TraceUpsert, async () => true);
|
||||
|
||||
const traceUpsertQueue = getTraceUpsertQueue();
|
||||
|
||||
@@ -47,10 +36,36 @@ describe.sequential("handle redis events", () => {
|
||||
},
|
||||
{
|
||||
timeout: 20_000,
|
||||
}
|
||||
},
|
||||
);
|
||||
}, 20_000);
|
||||
|
||||
test("handle no matching queue worker", async () => {
|
||||
// IngestionFlushQueue worker vs TraceUpsert producer
|
||||
WorkerManager.register(QueueName.IngestionFlushQueue, async () => true);
|
||||
|
||||
const traceUpsertQueue = getTraceUpsertQueue();
|
||||
|
||||
expect(traceUpsertQueue).toBeDefined();
|
||||
|
||||
const job = await traceUpsertQueue?.add(QueueJobs.TraceUpsert, {
|
||||
id: randomUUID(),
|
||||
timestamp: new Date(),
|
||||
payload: {
|
||||
projectId: "project-id",
|
||||
traceId: "trace-id",
|
||||
},
|
||||
name: QueueJobs.TraceUpsert as const,
|
||||
});
|
||||
|
||||
// Wait for 2s
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
// Job should still be waiting as there is no listener
|
||||
const jobState = await traceUpsertQueue?.getJobState(job!.id!);
|
||||
expect(jobState).toEqual("waiting");
|
||||
}, 5000);
|
||||
|
||||
// test("handle redis job failing", async () => {
|
||||
// vi.mock("../eval-service", () => ({
|
||||
// createEvalJobs: async ({
|
||||
|
||||
@@ -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
|
||||
|
||||
+57
-32
@@ -6,18 +6,21 @@ import MessageResponse from "./interfaces/MessageResponse";
|
||||
|
||||
require("dotenv").config();
|
||||
|
||||
import logger from "./logger";
|
||||
|
||||
import { evalJobCreator, evalJobExecutor } from "./queues/evalQueue";
|
||||
import { batchExportJobExecutor } from "./queues/batchExportQueue";
|
||||
import { ingestionQueueExecutor } from "./queues/ingestionFlushQueueExecutor";
|
||||
import { repeatQueueExecutor } from "./queues/repeatQueue";
|
||||
import { logQueueWorkerError } from "./utils/logQueueWorkerError";
|
||||
import {
|
||||
evalJobCreatorQueueProcessor,
|
||||
evalJobExecutorQueueProcessor,
|
||||
} from "./queues/evalQueue";
|
||||
import { batchExportQueueProcessor } from "./queues/batchExportQueue";
|
||||
import { ingestionFlushQueueProcessor } from "./queues/ingestionFlushQueueExecutor";
|
||||
import { repeatQueueProcessor } from "./queues/repeatQueue";
|
||||
import { onShutdown } from "./utils/shutdown";
|
||||
|
||||
import helmet from "helmet";
|
||||
import { legacyIngestionExecutor } from "./queues/legacyIngestionQueue";
|
||||
import { cloudUsageMeteringJobExecutor } from "./queues/cloudUsageMeteringQueue";
|
||||
import { legacyIngestionQueueProcessor } from "./queues/legacyIngestionQueue";
|
||||
import { cloudUsageMeteringQueueProcessor } from "./queues/cloudUsageMeteringQueue";
|
||||
import { WorkerManager } from "./queues/workerManager";
|
||||
import { QueueName } from "@langfuse/shared/src/server";
|
||||
import { env } from "./env";
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -35,32 +38,54 @@ app.use("/api", api);
|
||||
app.use(middlewares.notFound);
|
||||
app.use(middlewares.errorHandler);
|
||||
|
||||
logger.info(`Eval Job Creator started: ${evalJobCreator?.isRunning()}`);
|
||||
WorkerManager.register(QueueName.RepeatQueue, repeatQueueProcessor);
|
||||
|
||||
logger.info(`Eval Job Executor started: ${evalJobExecutor?.isRunning()}`);
|
||||
logger.info(
|
||||
`Batch Export Job Executor started: ${batchExportJobExecutor?.isRunning()}`
|
||||
);
|
||||
logger.info(
|
||||
`Repeat Queue Executor started: ${repeatQueueExecutor?.isRunning()}`
|
||||
);
|
||||
logger.info(
|
||||
`Flush Ingestion Queue Executor started: ${ingestionQueueExecutor?.isRunning()}`
|
||||
);
|
||||
logger.info(
|
||||
`Legacy Ingestion Executor started: ${legacyIngestionExecutor?.isRunning()}`
|
||||
);
|
||||
logger.info(
|
||||
`Cloud Usage Metering Job Executor started: ${cloudUsageMeteringJobExecutor?.isRunning()}`
|
||||
WorkerManager.register(QueueName.TraceUpsert, evalJobCreatorQueueProcessor, {
|
||||
concurrency: env.LANGFUSE_EVAL_CREATOR_WORKER_CONCURRENCY,
|
||||
});
|
||||
|
||||
WorkerManager.register(
|
||||
QueueName.EvaluationExecution,
|
||||
evalJobExecutorQueueProcessor,
|
||||
{
|
||||
concurrency: env.LANGFUSE_EVAL_EXECUTION_WORKER_CONCURRENCY,
|
||||
},
|
||||
);
|
||||
|
||||
evalJobCreator?.on("failed", logQueueWorkerError);
|
||||
evalJobExecutor?.on("failed", logQueueWorkerError);
|
||||
batchExportJobExecutor?.on("failed", logQueueWorkerError);
|
||||
repeatQueueExecutor?.on("failed", logQueueWorkerError);
|
||||
ingestionQueueExecutor?.on("failed", logQueueWorkerError);
|
||||
legacyIngestionExecutor?.on("failed", logQueueWorkerError);
|
||||
cloudUsageMeteringJobExecutor?.on("failed", logQueueWorkerError);
|
||||
WorkerManager.register(QueueName.BatchExport, batchExportQueueProcessor, {
|
||||
concurrency: 1, // only 1 job at a time
|
||||
limiter: {
|
||||
// execute 1 batch export in 5 seconds to avoid overloading the DB
|
||||
max: 1,
|
||||
duration: 5_000,
|
||||
},
|
||||
});
|
||||
|
||||
WorkerManager.register(
|
||||
QueueName.IngestionFlushQueue,
|
||||
ingestionFlushQueueProcessor,
|
||||
{
|
||||
concurrency: env.LANGFUSE_INGESTION_FLUSH_PROCESSING_CONCURRENCY,
|
||||
},
|
||||
);
|
||||
|
||||
if (env.STRIPE_SECRET_KEY) {
|
||||
WorkerManager.register(
|
||||
QueueName.CloudUsageMeteringQueue,
|
||||
cloudUsageMeteringQueueProcessor,
|
||||
{
|
||||
concurrency: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (env.QUEUE_CONSUMER_LEGACY_INGESTION_QUEUE_IS_ENABLED === "true") {
|
||||
WorkerManager.register(
|
||||
QueueName.LegacyIngestionQueue,
|
||||
legacyIngestionQueueProcessor,
|
||||
{ concurrency: env.LANGFUSE_LEGACY_INGESTION_WORKER_CONCURRENCY }, // n ingestion batches at a time
|
||||
);
|
||||
}
|
||||
|
||||
process.on("SIGINT", () => onShutdown("SIGINT"));
|
||||
process.on("SIGTERM", () => onShutdown("SIGTERM"));
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.76.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;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Job, Worker } from "bullmq";
|
||||
import { Job } from "bullmq";
|
||||
|
||||
import { BaseError, BatchExportStatus } from "@langfuse/shared";
|
||||
import { kyselyPrisma } from "@langfuse/shared/src/db";
|
||||
@@ -6,70 +6,48 @@ import { kyselyPrisma } from "@langfuse/shared/src/db";
|
||||
import {
|
||||
traceException,
|
||||
instrumentAsync,
|
||||
createNewRedisInstance,
|
||||
logger,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import logger from "../logger";
|
||||
import { QueueName, TQueueJobTypes } from "@langfuse/shared/src/server";
|
||||
import { handleBatchExportJob } from "../features/batchExport/handleBatchExportJob";
|
||||
import { SpanKind } from "@opentelemetry/api";
|
||||
|
||||
const createBatchExportJobExecutor = () => {
|
||||
const redisInstance = createNewRedisInstance();
|
||||
if (redisInstance) {
|
||||
return new Worker<TQueueJobTypes[QueueName.BatchExport]>(
|
||||
QueueName.BatchExport,
|
||||
async (job: Job<TQueueJobTypes[QueueName.BatchExport]>) => {
|
||||
return instrumentAsync(
|
||||
{
|
||||
name: "batchExportJobExecutor",
|
||||
spanKind: SpanKind.CONSUMER,
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
logger.info("Executing Batch Export Job", job.data.payload);
|
||||
await handleBatchExportJob(job.data.payload);
|
||||
export const batchExportQueueProcessor = async (
|
||||
job: Job<TQueueJobTypes[QueueName.BatchExport]>,
|
||||
) => {
|
||||
return instrumentAsync(
|
||||
{
|
||||
name: "batchExportJobExecutor",
|
||||
spanKind: SpanKind.CONSUMER,
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
logger.info("Executing Batch Export Job", job.data.payload);
|
||||
await handleBatchExportJob(job.data.payload);
|
||||
|
||||
logger.info("Finished Batch Export Job", job.data.payload);
|
||||
logger.info("Finished Batch Export Job", job.data.payload);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
const displayError =
|
||||
e instanceof BaseError
|
||||
? e.message
|
||||
: "An internal error occurred";
|
||||
return true;
|
||||
} catch (e) {
|
||||
const displayError =
|
||||
e instanceof BaseError ? e.message : "An internal error occurred";
|
||||
|
||||
await kyselyPrisma.$kysely
|
||||
.updateTable("batch_exports")
|
||||
.set("status", BatchExportStatus.FAILED)
|
||||
.set("finished_at", new Date())
|
||||
.set("log", displayError)
|
||||
.where("id", "=", job.data.payload.batchExportId)
|
||||
.where("project_id", "=", job.data.payload.projectId)
|
||||
.execute();
|
||||
await kyselyPrisma.$kysely
|
||||
.updateTable("batch_exports")
|
||||
.set("status", BatchExportStatus.FAILED)
|
||||
.set("finished_at", new Date())
|
||||
.set("log", displayError)
|
||||
.where("id", "=", job.data.payload.batchExportId)
|
||||
.where("project_id", "=", job.data.payload.projectId)
|
||||
.execute();
|
||||
|
||||
logger.error(
|
||||
e,
|
||||
`Failed Batch Export job for id ${job.data.payload.batchExportId} ${e}`
|
||||
);
|
||||
traceException(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
logger.error(
|
||||
`Failed Batch Export job for id ${job.data.payload.batchExportId}`,
|
||||
e,
|
||||
);
|
||||
},
|
||||
{
|
||||
connection: redisInstance,
|
||||
concurrency: 1, // only 1 job at a time
|
||||
limiter: {
|
||||
// execute 1 batch export in 5 seconds to avoid overloading the DB
|
||||
max: 1,
|
||||
duration: 5_000,
|
||||
},
|
||||
traceException(e);
|
||||
throw e;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const batchExportJobExecutor = createBatchExportJobExecutor();
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Queue, Worker } from "bullmq";
|
||||
import logger from "../logger";
|
||||
import { Processor, Queue } from "bullmq";
|
||||
import {
|
||||
redis,
|
||||
logger,
|
||||
QueueName,
|
||||
QueueJobs,
|
||||
instrumentAsync,
|
||||
createNewRedisInstance,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { handleCloudUsageMeteringJob } from "../ee/cloudUsageMetering/handleCloudUsageMeteringJob";
|
||||
import { env } from "../env";
|
||||
@@ -23,7 +22,7 @@ if (cloudUsageMeteringQueue) {
|
||||
{},
|
||||
{
|
||||
repeat: { pattern: "5 * * * *" },
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// add a job to the queue to start the job immediately in case we need to catch up
|
||||
@@ -35,35 +34,19 @@ if (cloudUsageMeteringQueue) {
|
||||
});
|
||||
}
|
||||
|
||||
const createCloudUsageMeteringJobExecutor = () => {
|
||||
const redisInstance = createNewRedisInstance();
|
||||
if (env.STRIPE_SECRET_KEY && redisInstance) {
|
||||
return new Worker(
|
||||
QueueName.CloudUsageMeteringQueue,
|
||||
async (job) => {
|
||||
if (job.name === QueueJobs.CloudUsageMeteringJob) {
|
||||
return instrumentAsync(
|
||||
{ name: "cloudUsageMeteringJobExecutor" },
|
||||
async () => {
|
||||
logger.info("Executing Cloud Usage Metering Job", job.data);
|
||||
try {
|
||||
return await handleCloudUsageMeteringJob(job);
|
||||
} catch (error) {
|
||||
logger.error("Error executing Cloud Usage Metering Job", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
export const cloudUsageMeteringQueueProcessor: Processor = async (job) => {
|
||||
if (job.name === QueueJobs.CloudUsageMeteringJob) {
|
||||
return instrumentAsync(
|
||||
{ name: "cloudUsageMeteringJobExecutor" },
|
||||
async () => {
|
||||
logger.info("Executing Cloud Usage Metering Job", job.data);
|
||||
try {
|
||||
return await handleCloudUsageMeteringJob(job);
|
||||
} catch (error) {
|
||||
logger.error("Error executing Cloud Usage Metering Job", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
{
|
||||
connection: redisInstance,
|
||||
concurrency: 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const cloudUsageMeteringJobExecutor =
|
||||
createCloudUsageMeteringJobExecutor();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user