Support AI session log streaming

This commit is contained in:
Owen
2026-08-21 17:22:00 -04:00
parent eca1c9044c
commit 19ce236262
10 changed files with 112 additions and 10 deletions
+2
View File
@@ -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",
+3
View File
@@ -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),
+3
View File
@@ -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 : "";
+3 -2
View File
@@ -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)
+29 -1
View File
@@ -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>
+28 -1
View File
@@ -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>