mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-26 22:15:01 +02:00
@@ -0,0 +1,15 @@
|
|||||||
|
import { gzipSync, gunzipSync } from "zlib";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gzip a string and return it as base64 so it can be stored in a TEXT column.
|
||||||
|
*/
|
||||||
|
export function compressText(value: string): string {
|
||||||
|
return gzipSync(Buffer.from(value, "utf8")).toString("base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse of compressText - base64-decode and gunzip back to the original string.
|
||||||
|
*/
|
||||||
|
export function decompressText(value: string): string {
|
||||||
|
return gunzipSync(Buffer.from(value, "base64")).toString("utf8");
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { and, eq, gt, desc, max, sql } from "drizzle-orm";
|
import { and, eq, gt, desc, max, sql } from "drizzle-orm";
|
||||||
import { decrypt } from "@server/lib/crypto";
|
import { decrypt } from "@server/lib/crypto";
|
||||||
|
import { decompressText } from "@server/lib/textCompression";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import {
|
import {
|
||||||
LogType,
|
LogType,
|
||||||
@@ -680,8 +681,8 @@ export class LogStreamingManager {
|
|||||||
Record<string, unknown> & { id: number }
|
Record<string, unknown> & { id: number }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
case "aiSession":
|
case "aiSession": {
|
||||||
return (await logsDb
|
const rows = (await logsDb
|
||||||
.select()
|
.select()
|
||||||
.from(aiSessionLog)
|
.from(aiSessionLog)
|
||||||
.where(
|
.where(
|
||||||
@@ -694,6 +695,33 @@ export class LogStreamingManager {
|
|||||||
.limit(limit)) as Array<
|
.limit(limit)) as Array<
|
||||||
Record<string, unknown> & { id: number }
|
Record<string, unknown> & { id: number }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
const compressedFields = [
|
||||||
|
"requestBody",
|
||||||
|
"responseBody",
|
||||||
|
"normalizedRequest",
|
||||||
|
"normalizedResponse"
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
for (const field of compressedFields) {
|
||||||
|
const value = row[field];
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
row[field] = decompressText(value);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to decompress AI session log field ${field}`,
|
||||||
|
{ error }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { and, eq, lt } from "drizzle-orm";
|
|||||||
import cache from "#private/lib/cache";
|
import cache from "#private/lib/cache";
|
||||||
import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs";
|
import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs";
|
||||||
import { sanitizeString } from "@server/lib/sanitize";
|
import { sanitizeString } from "@server/lib/sanitize";
|
||||||
|
import { compressText } from "@server/lib/textCompression";
|
||||||
import type { AiCapability } from "@server/lib/aiCapabilities";
|
import type { AiCapability } from "@server/lib/aiCapabilities";
|
||||||
import {
|
import {
|
||||||
normalizeAiRequest,
|
normalizeAiRequest,
|
||||||
@@ -151,7 +152,7 @@ async function getRetentionDays(orgId: string): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
|
export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
|
||||||
const cutoffTimestamp = calculateCutoffTimestamp(retentionDays) * 1000;
|
const cutoffTimestamp = calculateCutoffTimestamp(retentionDays);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await logsDb
|
await logsDb
|
||||||
@@ -255,13 +256,19 @@ export function logAiSession(data: {
|
|||||||
),
|
),
|
||||||
requestedModel: sanitizeString(data.requestedModel),
|
requestedModel: sanitizeString(data.requestedModel),
|
||||||
isStream: data.isStream,
|
isStream: data.isStream,
|
||||||
requestBody: sanitizeString(requestBodyText.value),
|
requestBody: compressText(
|
||||||
responseBody: sanitizeString(responseBodyText.value),
|
sanitizeString(requestBodyText.value)
|
||||||
|
),
|
||||||
|
responseBody: compressText(
|
||||||
|
sanitizeString(responseBodyText.value)
|
||||||
|
),
|
||||||
normalizedRequest: normalizedRequestText
|
normalizedRequest: normalizedRequestText
|
||||||
? sanitizeString(normalizedRequestText.value)
|
? compressText(sanitizeString(normalizedRequestText.value))
|
||||||
: undefined,
|
: undefined,
|
||||||
normalizedResponse: normalizedResponseText
|
normalizedResponse: normalizedResponseText
|
||||||
? sanitizeString(normalizedResponseText.value)
|
? compressText(
|
||||||
|
sanitizeString(normalizedResponseText.value)
|
||||||
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
truncated:
|
truncated:
|
||||||
requestBodyText.truncated ||
|
requestBodyText.truncated ||
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
queryAiSessionLogsQuery,
|
queryAiSessionLogsQuery,
|
||||||
queryAiSessionLogsParams,
|
queryAiSessionLogsParams,
|
||||||
queryAiSession,
|
queryAiSession,
|
||||||
countAiSessionQuery
|
countAiSessionQuery,
|
||||||
|
decompressAiSessionLogRow
|
||||||
} from "./queryAiSessionLog";
|
} from "./queryAiSessionLog";
|
||||||
import { generateCSV } from "./generateCSV";
|
import { generateCSV } from "./generateCSV";
|
||||||
|
|
||||||
@@ -87,7 +88,9 @@ export async function exportAiSessionLogs(
|
|||||||
|
|
||||||
const baseQuery = queryAiSession(data);
|
const baseQuery = queryAiSession(data);
|
||||||
|
|
||||||
const log = await baseQuery.limit(MAX_EXPORT_LIMIT);
|
const log = (await baseQuery.limit(MAX_EXPORT_LIMIT)).map(
|
||||||
|
decompressAiSessionLogRow
|
||||||
|
);
|
||||||
|
|
||||||
const csvData = generateCSV(log);
|
const csvData = generateCSV(log);
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { AI_CAPABILITIES } from "@server/lib/aiCapabilities";
|
|||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||||
|
import { decompressText } from "@server/lib/textCompression";
|
||||||
|
|
||||||
export const queryAiSessionLogsQuery = z.strictObject({
|
export const queryAiSessionLogsQuery = z.strictObject({
|
||||||
// iso string just validate its a parseable date
|
// iso string just validate its a parseable date
|
||||||
@@ -166,6 +167,35 @@ export function queryAiSession(data: Q) {
|
|||||||
.orderBy(desc(aiSessionLog.createdAt));
|
.orderBy(desc(aiSessionLog.createdAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decompressField(value: string | null): string | null {
|
||||||
|
if (value == null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return decompressText(value);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to decompress AI session log field", { error });
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decompressAiSessionLogRow<
|
||||||
|
T extends {
|
||||||
|
requestBody: string | null;
|
||||||
|
responseBody: string | null;
|
||||||
|
normalizedRequest: string | null;
|
||||||
|
normalizedResponse: string | null;
|
||||||
|
}
|
||||||
|
>(row: T): T {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
requestBody: decompressField(row.requestBody),
|
||||||
|
responseBody: decompressField(row.responseBody),
|
||||||
|
normalizedRequest: decompressField(row.normalizedRequest),
|
||||||
|
normalizedResponse: decompressField(row.normalizedResponse)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function enrichWithDetails(
|
async function enrichWithDetails(
|
||||||
logs: Awaited<ReturnType<typeof queryAiSession>>
|
logs: Awaited<ReturnType<typeof queryAiSession>>
|
||||||
) {
|
) {
|
||||||
@@ -620,7 +650,9 @@ export async function queryAiSessionLogs(
|
|||||||
|
|
||||||
const baseQuery = queryAiSession(data);
|
const baseQuery = queryAiSession(data);
|
||||||
|
|
||||||
const logsRaw = await baseQuery.limit(data.limit).offset(data.offset);
|
const logsRaw = (
|
||||||
|
await baseQuery.limit(data.limit).offset(data.offset)
|
||||||
|
).map(decompressAiSessionLogRow);
|
||||||
|
|
||||||
const log = await enrichWithDetails(logsRaw);
|
const log = await enrichWithDetails(logsRaw);
|
||||||
|
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ export default async function migration() {
|
|||||||
sql`ALTER TABLE "subscriptions" ADD COLUMN "override" boolean DEFAULT false;`
|
sql`ALTER TABLE "subscriptions" ADD COLUMN "override" boolean DEFAULT false;`
|
||||||
);
|
);
|
||||||
await db.execute(
|
await db.execute(
|
||||||
sql`ALTER TABLE "orgs" ADD COLUMN "settingsLogRetentionDaysAISessions" integer DEFAULT 7 NOT NULL;`
|
sql`ALTER TABLE "orgs" ADD COLUMN "settingsLogRetentionDaysAISessions" integer DEFAULT 0 NOT NULL;`
|
||||||
);
|
);
|
||||||
await db.execute(
|
await db.execute(
|
||||||
sql`ALTER TABLE "siteResources" ADD COLUMN "requiresExitNodeConnection" boolean DEFAULT false NOT NULL;`
|
sql`ALTER TABLE "siteResources" ADD COLUMN "requiresExitNodeConnection" boolean DEFAULT false NOT NULL;`
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ export default async function migration() {
|
|||||||
`ALTER TABLE 'clients' ADD 'exitNodeSubnet' text;`
|
`ALTER TABLE 'clients' ADD 'exitNodeSubnet' text;`
|
||||||
).run();
|
).run();
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`ALTER TABLE 'orgs' ADD 'settingsLogRetentionDaysAISessions' integer DEFAULT 7 NOT NULL;`
|
`ALTER TABLE 'orgs' ADD 'settingsLogRetentionDaysAISessions' integer DEFAULT 0 NOT NULL;`
|
||||||
).run();
|
).run();
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`ALTER TABLE 'siteResources' ADD 'requiresExitNodeConnection' integer DEFAULT false NOT NULL;`
|
`ALTER TABLE 'siteResources' ADD 'requiresExitNodeConnection' integer DEFAULT false NOT NULL;`
|
||||||
|
|||||||
Reference in New Issue
Block a user