mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-22 12:10:35 +02:00
Support AI session log streaming
This commit is contained in:
@@ -4087,6 +4087,8 @@
|
|||||||
"httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.",
|
"httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.",
|
||||||
"httpDestRequestLogsTitle": "HTTP Request Logs",
|
"httpDestRequestLogsTitle": "HTTP Request Logs",
|
||||||
"httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.",
|
"httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.",
|
||||||
|
"httpDestAISessionLogsTitle": "AI Session Logs",
|
||||||
|
"httpDestAISessionLogsDescription": "AI gateway request and response sessions, including prompts, model responses, and token usage.",
|
||||||
"httpDestSaveChanges": "Save Changes",
|
"httpDestSaveChanges": "Save Changes",
|
||||||
"httpDestCreateDestination": "Create Destination",
|
"httpDestCreateDestination": "Create Destination",
|
||||||
"httpDestUpdatedSuccess": "Destination updated successfully",
|
"httpDestUpdatedSuccess": "Destination updated successfully",
|
||||||
|
|||||||
@@ -468,6 +468,9 @@ export const eventStreamingDestinations = pgTable(
|
|||||||
sendRequestLogs: boolean("sendRequestLogs").notNull().default(false),
|
sendRequestLogs: boolean("sendRequestLogs").notNull().default(false),
|
||||||
sendActionLogs: boolean("sendActionLogs").notNull().default(false),
|
sendActionLogs: boolean("sendActionLogs").notNull().default(false),
|
||||||
sendAccessLogs: boolean("sendAccessLogs").notNull().default(false),
|
sendAccessLogs: boolean("sendAccessLogs").notNull().default(false),
|
||||||
|
sendAISessionLogs: boolean("sendAISessionLogs")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
type: varchar("type", { length: 50 }).notNull(), // e.g. "http", "kafka", etc.
|
type: varchar("type", { length: 50 }).notNull(), // e.g. "http", "kafka", etc.
|
||||||
config: text("config").notNull(), // JSON string with the configuration for the destination
|
config: text("config").notNull(), // JSON string with the configuration for the destination
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
|||||||
@@ -459,6 +459,9 @@ export const eventStreamingDestinations = sqliteTable(
|
|||||||
sendAccessLogs: integer("sendAccessLogs", { mode: "boolean" })
|
sendAccessLogs: integer("sendAccessLogs", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
|
sendAISessionLogs: integer("sendAISessionLogs", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
type: text("type").notNull(), // e.g. "http", "kafka", etc.
|
type: text("type").notNull(), // e.g. "http", "kafka", etc.
|
||||||
config: text("config").notNull(), // JSON string with the configuration for the destination
|
config: text("config").notNull(), // JSON string with the configuration for the destination
|
||||||
enabled: integer("enabled", { mode: "boolean" })
|
enabled: integer("enabled", { mode: "boolean" })
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import {
|
|||||||
requestAuditLog,
|
requestAuditLog,
|
||||||
actionAuditLog,
|
actionAuditLog,
|
||||||
accessAuditLog,
|
accessAuditLog,
|
||||||
connectionAuditLog
|
connectionAuditLog,
|
||||||
|
aiSessionLog
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
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";
|
||||||
@@ -309,6 +310,7 @@ export class LogStreamingManager {
|
|||||||
if (dest.sendActionLogs) enabledTypes.push("action");
|
if (dest.sendActionLogs) enabledTypes.push("action");
|
||||||
if (dest.sendAccessLogs) enabledTypes.push("access");
|
if (dest.sendAccessLogs) enabledTypes.push("access");
|
||||||
if (dest.sendConnectionLogs) enabledTypes.push("connection");
|
if (dest.sendConnectionLogs) enabledTypes.push("connection");
|
||||||
|
if (dest.sendAISessionLogs) enabledTypes.push("aiSession");
|
||||||
|
|
||||||
if (enabledTypes.length === 0) return;
|
if (enabledTypes.length === 0) return;
|
||||||
|
|
||||||
@@ -585,6 +587,13 @@ export class LogStreamingManager {
|
|||||||
.where(eq(connectionAuditLog.orgId, orgId));
|
.where(eq(connectionAuditLog.orgId, orgId));
|
||||||
return row?.maxId ?? 0;
|
return row?.maxId ?? 0;
|
||||||
}
|
}
|
||||||
|
case "aiSession": {
|
||||||
|
const [row] = await logsDb
|
||||||
|
.select({ maxId: max(aiSessionLog.id) })
|
||||||
|
.from(aiSessionLog)
|
||||||
|
.where(eq(aiSessionLog.orgId, orgId));
|
||||||
|
return row?.maxId ?? 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -670,6 +679,21 @@ export class LogStreamingManager {
|
|||||||
.limit(limit)) as Array<
|
.limit(limit)) as Array<
|
||||||
Record<string, unknown> & { id: number }
|
Record<string, unknown> & { id: number }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
case "aiSession":
|
||||||
|
return (await logsDb
|
||||||
|
.select()
|
||||||
|
.from(aiSessionLog)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(aiSessionLog.orgId, orgId),
|
||||||
|
gt(aiSessionLog.id, afterId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(aiSessionLog.id)
|
||||||
|
.limit(limit)) as Array<
|
||||||
|
Record<string, unknown> & { id: number }
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,6 +718,14 @@ export class LogStreamingManager {
|
|||||||
timestamp =
|
timestamp =
|
||||||
typeof row.startedAt === "number" ? row.startedAt : 0;
|
typeof row.startedAt === "number" ? row.startedAt : 0;
|
||||||
break;
|
break;
|
||||||
|
case "aiSession":
|
||||||
|
// createdAt is stored as epoch milliseconds; normalise to
|
||||||
|
// epoch seconds to match the other log types.
|
||||||
|
timestamp =
|
||||||
|
typeof row.createdAt === "number"
|
||||||
|
? Math.floor(row.createdAt / 1000)
|
||||||
|
: 0;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const orgId = typeof row.orgId === "string" ? row.orgId : "";
|
const orgId = typeof row.orgId === "string" ? row.orgId : "";
|
||||||
|
|||||||
@@ -15,13 +15,14 @@
|
|||||||
// Log type identifiers
|
// Log type identifiers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export type LogType = "request" | "action" | "access" | "connection";
|
export type LogType = "request" | "action" | "access" | "connection" | "aiSession";
|
||||||
|
|
||||||
export const LOG_TYPES: LogType[] = [
|
export const LOG_TYPES: LogType[] = [
|
||||||
"request",
|
"request",
|
||||||
"action",
|
"action",
|
||||||
"access",
|
"access",
|
||||||
"connection"
|
"connection",
|
||||||
|
"aiSession"
|
||||||
];
|
];
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ const bodySchema = z.strictObject({
|
|||||||
sendConnectionLogs: z.boolean().optional().default(false),
|
sendConnectionLogs: z.boolean().optional().default(false),
|
||||||
sendRequestLogs: z.boolean().optional().default(false),
|
sendRequestLogs: z.boolean().optional().default(false),
|
||||||
sendActionLogs: z.boolean().optional().default(false),
|
sendActionLogs: z.boolean().optional().default(false),
|
||||||
sendAccessLogs: z.boolean().optional().default(false)
|
sendAccessLogs: z.boolean().optional().default(false),
|
||||||
|
sendAISessionLogs: z.boolean().optional().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateEventStreamingDestinationResponse = {
|
export type CreateEventStreamingDestinationResponse = {
|
||||||
@@ -122,7 +123,8 @@ export async function createEventStreamingDestination(
|
|||||||
sendAccessLogs: parsedBody.data.sendAccessLogs,
|
sendAccessLogs: parsedBody.data.sendAccessLogs,
|
||||||
sendActionLogs: parsedBody.data.sendActionLogs,
|
sendActionLogs: parsedBody.data.sendActionLogs,
|
||||||
sendConnectionLogs: parsedBody.data.sendConnectionLogs,
|
sendConnectionLogs: parsedBody.data.sendConnectionLogs,
|
||||||
sendRequestLogs: parsedBody.data.sendRequestLogs
|
sendRequestLogs: parsedBody.data.sendRequestLogs,
|
||||||
|
sendAISessionLogs: parsedBody.data.sendAISessionLogs
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export type ListEventStreamingDestinationsResponse = {
|
|||||||
sendRequestLogs: boolean;
|
sendRequestLogs: boolean;
|
||||||
sendActionLogs: boolean;
|
sendActionLogs: boolean;
|
||||||
sendAccessLogs: boolean;
|
sendAccessLogs: boolean;
|
||||||
|
sendAISessionLogs: boolean;
|
||||||
}[];
|
}[];
|
||||||
pagination: {
|
pagination: {
|
||||||
total: number;
|
total: number;
|
||||||
@@ -83,7 +84,8 @@ const ListEventStreamingDestinationsResponseDataSchema = z.object({
|
|||||||
sendConnectionLogs: z.boolean(),
|
sendConnectionLogs: z.boolean(),
|
||||||
sendRequestLogs: z.boolean(),
|
sendRequestLogs: z.boolean(),
|
||||||
sendActionLogs: z.boolean(),
|
sendActionLogs: z.boolean(),
|
||||||
sendAccessLogs: z.boolean()
|
sendAccessLogs: z.boolean(),
|
||||||
|
sendAISessionLogs: z.boolean()
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
pagination: z.object({
|
pagination: z.object({
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ const bodySchema = z.strictObject({
|
|||||||
sendConnectionLogs: z.boolean().optional(),
|
sendConnectionLogs: z.boolean().optional(),
|
||||||
sendRequestLogs: z.boolean().optional(),
|
sendRequestLogs: z.boolean().optional(),
|
||||||
sendActionLogs: z.boolean().optional(),
|
sendActionLogs: z.boolean().optional(),
|
||||||
sendAccessLogs: z.boolean().optional()
|
sendAccessLogs: z.boolean().optional(),
|
||||||
|
sendAISessionLogs: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UpdateEventStreamingDestinationResponse = {
|
export type UpdateEventStreamingDestinationResponse = {
|
||||||
@@ -125,7 +126,7 @@ export async function updateEventStreamingDestination(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { type, config: configToUpdate, enabled, sendAccessLogs, sendActionLogs, sendConnectionLogs, sendRequestLogs } = parsedBody.data;
|
const { type, config: configToUpdate, enabled, sendAccessLogs, sendActionLogs, sendConnectionLogs, sendRequestLogs, sendAISessionLogs } = parsedBody.data;
|
||||||
|
|
||||||
const updateData: Record<string, unknown> = {
|
const updateData: Record<string, unknown> = {
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now()
|
||||||
@@ -141,6 +142,7 @@ export async function updateEventStreamingDestination(
|
|||||||
if (sendActionLogs !== undefined) updateData.sendActionLogs = sendActionLogs;
|
if (sendActionLogs !== undefined) updateData.sendActionLogs = sendActionLogs;
|
||||||
if (sendConnectionLogs !== undefined) updateData.sendConnectionLogs = sendConnectionLogs;
|
if (sendConnectionLogs !== undefined) updateData.sendConnectionLogs = sendConnectionLogs;
|
||||||
if (sendRequestLogs !== undefined) updateData.sendRequestLogs = sendRequestLogs;
|
if (sendRequestLogs !== undefined) updateData.sendRequestLogs = sendRequestLogs;
|
||||||
|
if (sendAISessionLogs !== undefined) updateData.sendAISessionLogs = sendAISessionLogs;
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(eventStreamingDestinations)
|
.update(eventStreamingDestinations)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export interface Destination {
|
|||||||
sendActionLogs: boolean;
|
sendActionLogs: boolean;
|
||||||
sendConnectionLogs: boolean;
|
sendConnectionLogs: boolean;
|
||||||
sendRequestLogs: boolean;
|
sendRequestLogs: boolean;
|
||||||
|
sendAISessionLogs: boolean;
|
||||||
lastError: string | null;
|
lastError: string | null;
|
||||||
lastErrorAt: number | null;
|
lastErrorAt: number | null;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
@@ -180,6 +181,7 @@ export function HttpDestinationCredenza({
|
|||||||
const [sendActionLogs, setSendActionLogs] = useState(false);
|
const [sendActionLogs, setSendActionLogs] = useState(false);
|
||||||
const [sendConnectionLogs, setSendConnectionLogs] = useState(false);
|
const [sendConnectionLogs, setSendConnectionLogs] = useState(false);
|
||||||
const [sendRequestLogs, setSendRequestLogs] = useState(false);
|
const [sendRequestLogs, setSendRequestLogs] = useState(false);
|
||||||
|
const [sendAISessionLogs, setSendAISessionLogs] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -190,6 +192,7 @@ export function HttpDestinationCredenza({
|
|||||||
setSendActionLogs(editing?.sendActionLogs ?? false);
|
setSendActionLogs(editing?.sendActionLogs ?? false);
|
||||||
setSendConnectionLogs(editing?.sendConnectionLogs ?? false);
|
setSendConnectionLogs(editing?.sendConnectionLogs ?? false);
|
||||||
setSendRequestLogs(editing?.sendRequestLogs ?? false);
|
setSendRequestLogs(editing?.sendRequestLogs ?? false);
|
||||||
|
setSendAISessionLogs(editing?.sendAISessionLogs ?? false);
|
||||||
}
|
}
|
||||||
}, [open, editing]);
|
}, [open, editing]);
|
||||||
|
|
||||||
@@ -226,7 +229,8 @@ export function HttpDestinationCredenza({
|
|||||||
sendAccessLogs,
|
sendAccessLogs,
|
||||||
sendActionLogs,
|
sendActionLogs,
|
||||||
sendConnectionLogs,
|
sendConnectionLogs,
|
||||||
sendRequestLogs
|
sendRequestLogs,
|
||||||
|
sendAISessionLogs
|
||||||
};
|
};
|
||||||
if (editing) {
|
if (editing) {
|
||||||
await api.post(
|
await api.post(
|
||||||
@@ -778,6 +782,30 @@ export function HttpDestinationCredenza({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||||
|
<Checkbox
|
||||||
|
id="log-ai-session"
|
||||||
|
checked={sendAISessionLogs}
|
||||||
|
onCheckedChange={(v) =>
|
||||||
|
setSendAISessionLogs(v === true)
|
||||||
|
}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="log-ai-session"
|
||||||
|
className="text-sm font-medium cursor-pointer"
|
||||||
|
>
|
||||||
|
{t("httpDestAISessionLogsTitle")}
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{t(
|
||||||
|
"httpDestAISessionLogsDescription"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HorizontalTabs>
|
</HorizontalTabs>
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export function S3DestinationCredenza({
|
|||||||
const [sendActionLogs, setSendActionLogs] = useState(false);
|
const [sendActionLogs, setSendActionLogs] = useState(false);
|
||||||
const [sendConnectionLogs, setSendConnectionLogs] = useState(false);
|
const [sendConnectionLogs, setSendConnectionLogs] = useState(false);
|
||||||
const [sendRequestLogs, setSendRequestLogs] = useState(false);
|
const [sendRequestLogs, setSendRequestLogs] = useState(false);
|
||||||
|
const [sendAISessionLogs, setSendAISessionLogs] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -98,6 +99,7 @@ export function S3DestinationCredenza({
|
|||||||
setSendActionLogs(editing?.sendActionLogs ?? false);
|
setSendActionLogs(editing?.sendActionLogs ?? false);
|
||||||
setSendConnectionLogs(editing?.sendConnectionLogs ?? false);
|
setSendConnectionLogs(editing?.sendConnectionLogs ?? false);
|
||||||
setSendRequestLogs(editing?.sendRequestLogs ?? false);
|
setSendRequestLogs(editing?.sendRequestLogs ?? false);
|
||||||
|
setSendAISessionLogs(editing?.sendAISessionLogs ?? false);
|
||||||
}
|
}
|
||||||
}, [open, editing]);
|
}, [open, editing]);
|
||||||
|
|
||||||
@@ -121,7 +123,8 @@ export function S3DestinationCredenza({
|
|||||||
sendAccessLogs,
|
sendAccessLogs,
|
||||||
sendActionLogs,
|
sendActionLogs,
|
||||||
sendConnectionLogs,
|
sendConnectionLogs,
|
||||||
sendRequestLogs
|
sendRequestLogs,
|
||||||
|
sendAISessionLogs
|
||||||
};
|
};
|
||||||
if (editing) {
|
if (editing) {
|
||||||
await api.post(
|
await api.post(
|
||||||
@@ -510,6 +513,30 @@ export function S3DestinationCredenza({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||||
|
<Checkbox
|
||||||
|
id="s3-log-ai-session"
|
||||||
|
checked={sendAISessionLogs}
|
||||||
|
onCheckedChange={(v) =>
|
||||||
|
setSendAISessionLogs(v === true)
|
||||||
|
}
|
||||||
|
className="mt-0.5"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Label
|
||||||
|
htmlFor="s3-log-ai-session"
|
||||||
|
className="cursor-pointer font-medium"
|
||||||
|
>
|
||||||
|
{t("httpDestAISessionLogsTitle")}
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{t(
|
||||||
|
"httpDestAISessionLogsDescription"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HorizontalTabs>
|
</HorizontalTabs>
|
||||||
|
|||||||
Reference in New Issue
Block a user