Compare commits

...

3 Commits

Author SHA1 Message Date
shatfield4 f7940c1fea rename files to Logging to prevent getting gitignore 2024-02-02 18:38:32 -08:00
shatfield4 dac7c355d4 UI for log rows 2024-02-02 18:32:20 -08:00
shatfield4 0b313e7078 WIP add logging 2024-02-02 16:49:04 -08:00
16 changed files with 329 additions and 43 deletions

View File

@ -20,7 +20,7 @@ const AdminUsers = lazy(() => import("@/pages/Admin/Users"));
const AdminInvites = lazy(() => import("@/pages/Admin/Invitations"));
const AdminWorkspaces = lazy(() => import("@/pages/Admin/Workspaces"));
const AdminSystem = lazy(() => import("@/pages/Admin/System"));
const AdminLogs = lazy(() => import("@/pages/Admin/Logs"));
const AdminLogs = lazy(() => import("@/pages/Admin/Logging"));
const GeneralChats = lazy(() => import("@/pages/GeneralSettings/Chats"));
const GeneralAppearance = lazy(
() => import("@/pages/GeneralSettings/Appearance")

View File

@ -161,7 +161,7 @@ export default function SettingsSidebar() {
icon={<Notepad className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
allowedRole={["admin"]}
/>
</div>
</div>

View File

@ -389,6 +389,18 @@ const System = {
return [];
});
},
logs: async (offset = 0) => {
return await fetch(`${API_BASE}/system/logs`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ offset }),
})
.then((res) => res.json())
.catch((e) => {
console.error(e);
return [];
});
},
deleteChat: async (chatId) => {
return await fetch(`${API_BASE}/system/workspace-chats/${chatId}`, {
method: "DELETE",

View File

@ -0,0 +1,62 @@
import { CaretDown } from "@phosphor-icons/react";
import { useState } from "react";
export default function LogRow({ log }) {
const [expanded, setExpanded] = useState(false);
const handleRowClick = () => {
if (log.metadata !== "{}") {
setExpanded(!expanded);
}
};
return (
<>
<tr
onClick={handleRowClick}
className="bg-transparent text-white text-opacity-80 text-sm font-medium cursor-pointer"
>
<td className="px-6 py-4 font-medium whitespace-nowrap text-white">
{log.id}
</td>
<td className="px-6 py-4 font-medium whitespace-nowrap text-white flex items-center">
<span className="rounded-full bg-sky-600/20 px-2 py-0.5 text-sm font-medium text-sky-400 shadow-sm">
{log.event}
</span>
</td>
<td className="px-6 py-4 border-transparent transform transition-transform duration-200">
{log.user.username}
</td>
<td className="px-6 py-4 border-transparent transform transition-transform duration-200">
{log.occurredAt}
</td>
{log.metadata !== "{}" && (
<td
className={`flex items-center justify-center transform transition-transform duration-200 hover:scale-105 ${
expanded ? "rotate-0" : "rotate-90"
}`}
>
<CaretDown weight="bold" size={20} />
</td>
)}
</tr>
{expanded && (
<tr className="bg-sidebar">
<td
colSpan="2"
className="px-6 py-4 font-medium text-white rounded-l-2xl"
>
Event Metadata
</td>
<td colSpan="4" className="px-6 py-4 rounded-r-2xl">
<div className="w-full rounded-lg bg-main-2 p-2 text-white shadow-sm border-white border bg-opacity-10">
<pre className="overflow-scroll">
{JSON.stringify(JSON.parse(log.metadata), null, 2)}
</pre>
</div>
</td>
</tr>
)}
</>
);
}

View File

@ -0,0 +1,119 @@
import Sidebar, { SidebarMobileHeader } from "@/components/SettingsSidebar";
import useQuery from "@/hooks/useQuery";
import System from "@/models/system";
import { useEffect, useState } from "react";
import { isMobile } from "react-device-detect";
import * as Skeleton from "react-loading-skeleton";
import LogRow from "./LogRow";
export default function AdminLogs() {
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
{!isMobile && <Sidebar />}
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
>
{isMobile && <SidebarMobileHeader />}
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="items-center flex gap-x-4">
<p className="text-2xl font-semibold text-white">Logs</p>
</div>
<p className="text-sm font-base text-white text-opacity-60">
View all logs and events happening on this instance.
</p>
</div>
<ChatsContainer />
</div>
</div>
</div>
);
}
function ChatsContainer() {
const query = useQuery();
const [loading, setLoading] = useState(true);
// const [chats, setChats] = useState([]);
const [logs, setLogs] = useState([]);
const [offset, setOffset] = useState(Number(query.get("offset") || 0));
const [canNext, setCanNext] = useState(false);
const handlePrevious = () => {
setOffset(Math.max(offset - 1, 0));
};
const handleNext = () => {
setOffset(offset + 1);
};
useEffect(() => {
async function fetchLogs() {
const { logs: _logs, hasPages = false } = await System.logs(offset);
console.log(_logs);
setLogs(_logs);
setCanNext(hasPages);
setLoading(false);
}
fetchLogs();
}, [offset]);
if (loading) {
return (
<Skeleton.default
height="80vh"
width="100%"
highlightColor="#3D4147"
baseColor="#2C2F35"
count={1}
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
containerClassName="flex w-full"
/>
);
}
return (
<>
<table className="md:w-3/4 w-full text-sm text-left rounded-lg mt-5">
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
Id
</th>
<th scope="col" className="px-6 py-3">
Event Type
</th>
<th scope="col" className="px-6 py-3">
User
</th>
<th scope="col" className="px-6 py-3">
Occurred At
</th>
<th scope="col" className="px-6 py-3 rounded-tr-lg">
{" "}
</th>
</tr>
</thead>
<tbody>
{!!logs && logs.map((log) => <LogRow key={log.id} log={log} />)}
</tbody>
</table>
<div className="flex w-full justify-between items-center">
<button
onClick={handlePrevious}
className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible"
disabled={offset === 0}
>
{" "}
Previous Page
</button>
<button
onClick={handleNext}
className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible"
disabled={!canNext}
>
Next Page
</button>
</div>
</>
);
}

View File

@ -12,6 +12,7 @@ const {
findDocumentInDocuments,
} = require("../../../utils/files");
const { reqBody } = require("../../../utils/http");
const { EventLogs } = require("../../../models/eventLogs");
const { handleUploads } = setupMulter();
function apiDocumentEndpoints(app) {
@ -22,7 +23,7 @@ function apiDocumentEndpoints(app) {
[validApiKey],
handleUploads.single("file"),
async (request, response) => {
/*
/*
#swagger.tags = ['Documents']
#swagger.description = 'Upload a new file to AnythingLLM to be parsed and prepared for embedding.'
#swagger.requestBody = {
@ -68,9 +69,9 @@ function apiDocumentEndpoints(app) {
]
}
}
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
@ -105,6 +106,7 @@ function apiDocumentEndpoints(app) {
`Document ${originalname} uploaded processed and successfully. It is now available in documents.`
);
await Telemetry.sendTelemetry("document_uploaded");
await EventLogs.logEvent("document_uploaded");
response.status(200).json({ success: true, error: null, documents });
} catch (e) {
console.log(e.message, e);
@ -117,7 +119,7 @@ function apiDocumentEndpoints(app) {
"/v1/document/upload-link",
[validApiKey],
async (request, response) => {
/*
/*
#swagger.tags = ['Documents']
#swagger.description = 'Upload a valid URL for AnythingLLM to scrape and prepare for embedding.'
#swagger.requestBody = {
@ -132,7 +134,7 @@ function apiDocumentEndpoints(app) {
"link": "https://useanything.com"
}
}
}
}
}
}
#swagger.responses[200] = {
@ -161,9 +163,9 @@ function apiDocumentEndpoints(app) {
]
}
}
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
@ -197,6 +199,7 @@ function apiDocumentEndpoints(app) {
`Link ${link} uploaded processed and successfully. It is now available in documents.`
);
await Telemetry.sendTelemetry("document_uploaded");
await EventLogs.logEvent("document_uploaded");
response.status(200).json({ success: true, error: null, documents });
} catch (e) {
console.log(e.message, e);
@ -206,7 +209,7 @@ function apiDocumentEndpoints(app) {
);
app.get("/v1/documents", [validApiKey], async (_, response) => {
/*
/*
#swagger.tags = ['Documents']
#swagger.description = 'List of all locally-stored documents in instance'
#swagger.responses[200] = {
@ -231,9 +234,9 @@ function apiDocumentEndpoints(app) {
}
}
}
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
@ -250,7 +253,7 @@ function apiDocumentEndpoints(app) {
});
app.get("/v1/document/:docName", [validApiKey], async (request, response) => {
/*
/*
#swagger.tags = ['Documents']
#swagger.description = 'Get a single document by its unique AnythingLLM document name'
#swagger.parameters['docName'] = {
@ -281,9 +284,9 @@ function apiDocumentEndpoints(app) {
}
}
}
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
@ -308,7 +311,7 @@ function apiDocumentEndpoints(app) {
"/v1/document/accepted-file-types",
[validApiKey],
async (_, response) => {
/*
/*
#swagger.tags = ['Documents']
#swagger.description = 'Check available filetypes and MIMEs that can be uploaded.'
#swagger.responses[200] = {
@ -337,9 +340,9 @@ function apiDocumentEndpoints(app) {
}
}
}
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"

View File

@ -16,6 +16,7 @@ const {
writeResponseChunk,
VALID_CHAT_MODE,
} = require("../../../utils/chats/stream");
const { EventLogs } = require("../../../models/eventLogs");
function apiWorkspaceEndpoints(app) {
if (!app) return;
@ -73,6 +74,12 @@ function apiWorkspaceEndpoints(app) {
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
});
await EventLogs.logEvent("workspace_created", {
multiUserMode: multiUserMode(response),
LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
});
response.status(200).json({ workspace, message });
} catch (e) {
console.log(e.message, e);
@ -519,6 +526,11 @@ function apiWorkspaceEndpoints(app) {
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
});
await EventLogs.logEvent("sent_chat", {
LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
});
response.status(200).json({ ...result });
} catch (e) {
response.status(500).json({
@ -637,6 +649,11 @@ function apiWorkspaceEndpoints(app) {
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
});
await EventLogs.logEvent("sent_chat", {
LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
});
response.end();
} catch (e) {
console.error(e);

View File

@ -99,16 +99,17 @@ function chatEndpoints(app) {
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
});
await EventLogs.logEvent({
event: "sent_chat",
userId: user?.id || null,
metadata: {
await EventLogs.logEvent(
"sent_chat",
{
multiUserMode: multiUserMode(response),
LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
},
});
user?.id
);
response.end();
} catch (e) {
console.error(e);

View File

@ -1,3 +1,4 @@
const { EventLogs } = require("../../models/eventLogs");
const { Telemetry } = require("../../models/telemetry");
const {
forwardExtensionRequest,
@ -42,6 +43,9 @@ function extensionEndpoints(app) {
await Telemetry.sendTelemetry("extension_invoked", {
type: "github_repo",
});
await EventLogs.logEvent("extension_invoked", {
type: "github_repo",
});
response.status(200).json(responseFromProcessor);
} catch (e) {
console.error(e);
@ -63,6 +67,9 @@ function extensionEndpoints(app) {
await Telemetry.sendTelemetry("extension_invoked", {
type: "youtube_transcript",
});
await EventLogs.logEvent("extension_invoked", {
type: "youtube_transcript",
});
response.status(200).json(responseFromProcessor);
} catch (e) {
console.error(e);

View File

@ -150,11 +150,12 @@ function systemEndpoints(app) {
existingUser?.id
);
await EventLogs.logEvent({
event: "login_event",
userId: existingUser?.id || null,
metadata: {},
});
await EventLogs.logEvent(
"login_event",
{ multiUserMode: false },
existingUser?.id
);
response.status(200).json({
valid: true,
user: existingUser,
@ -182,6 +183,7 @@ function systemEndpoints(app) {
}
await Telemetry.sendTelemetry("login_event", { multiUserMode: false });
await EventLogs.logEvent("login_event", { multiUserMode: false });
response.status(200).json({
valid: true,
token: makeJWT({ p: password }, "30d"),
@ -371,6 +373,9 @@ function systemEndpoints(app) {
await Telemetry.sendTelemetry("enabled_multi_user_mode", {
multiUserMode: true,
});
await EventLogs.logEvent("enabled_multi_user_mode", {
multiUserMode: true,
});
response.status(200).json({ success: !!user, error });
} catch (e) {
await User.delete({});
@ -751,6 +756,26 @@ function systemEndpoints(app) {
}
);
app.post(
"/system/logs",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
async (request, response) => {
try {
const { offset = 0, limit = 20 } = reqBody(request);
const logs = await EventLogs.whereWithData({}, limit, offset * limit, {
id: "desc",
});
const totalLogs = await EventLogs.count();
const hasPages = totalLogs > (offset + 1) * limit;
response.status(200).json({ logs: logs, hasPages, totalLogs });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
}
}
);
app.post(
"/system/workspace-chats",
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],

View File

@ -17,6 +17,7 @@ const {
flexUserRoleValid,
ROLES,
} = require("../utils/middleware/multiUserProtected");
const { EventLogs } = require("../models/eventLogs");
const { handleUploads } = setupMulter();
function workspaceEndpoints(app) {
@ -40,6 +41,17 @@ function workspaceEndpoints(app) {
},
user?.id
);
await EventLogs.logEvent(
"workspace_created",
{
multiUserMode: multiUserMode(response),
LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
},
user?.id
);
if (onboardingComplete === true)
await Telemetry.sendTelemetry("onboarding_complete");
@ -109,6 +121,7 @@ function workspaceEndpoints(app) {
`Document ${originalname} uploaded processed and successfully. It is now available in documents.`
);
await Telemetry.sendTelemetry("document_uploaded");
await EventLogs.logEvent("document_uploaded");
response.status(200).json({ success: true, error: null });
}
);
@ -141,6 +154,7 @@ function workspaceEndpoints(app) {
`Link ${link} uploaded processed and successfully. It is now available in documents.`
);
await Telemetry.sendTelemetry("link_uploaded");
await EventLogs.logEvent("link_uploaded");
response.status(200).json({ success: true, error: null });
}
);

View File

@ -1,5 +1,6 @@
const { Telemetry } = require("./telemetry");
const prisma = require("../utils/prisma");
const { EventLogs } = require("./eventLogs");
const ApiKey = {
tablename: "api_keys",
@ -20,6 +21,7 @@ const ApiKey = {
});
await Telemetry.sendTelemetry("api_key_created");
await EventLogs.logEvent("api_key_created");
return { apiKey, error: null };
} catch (error) {
console.error("FAILED TO CREATE API KEY.", error.message);

View File

@ -3,6 +3,7 @@ const { v4: uuidv4 } = require("uuid");
const { getVectorDbClass } = require("../utils/helpers");
const prisma = require("../utils/prisma");
const { Telemetry } = require("./telemetry");
const { EventLogs } = require("./eventLogs");
const Document = {
forWorkspace: async function (workspaceId = null) {
@ -84,6 +85,11 @@ const Document = {
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
});
await EventLogs.logEvent("documents_embedded_in_workspace", {
LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
});
return { failedToEmbed, errors: Array.from(errors), embedded };
},
@ -119,6 +125,11 @@ const Document = {
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
});
await EventLogs.logEvent("documents_removed_in_workspace", {
LLMSelection: process.env.LLM_PROVIDER || "openai",
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
});
return true;
},

View File

@ -1,22 +1,14 @@
const prisma = require("../utils/prisma");
const EventLogs = {
logEvent: async function ({
event,
description = null,
metadata = null,
userId = null,
ipAddress = null,
}) {
logEvent: async function (event, metadata = {}, userId = null) {
try {
const eventLog = await prisma.event_logs.create({
data: {
event,
description,
metadata: metadata ? JSON.stringify(metadata) : null,
userId,
userId: userId ? Number(userId) : null,
occurredAt: new Date(),
ipAddress,
},
});
return { eventLog, message: null };
@ -80,6 +72,31 @@ const EventLogs = {
}
},
whereWithData: async function (
clause = {},
limit = null,
offset = null,
orderBy = null
) {
const { User } = require("./user");
try {
const results = await this.where(clause, limit, orderBy, offset);
for (const res of results) {
const user = res.userId ? await User.get({ id: res.userId }) : null;
res.user = user
? { username: user.username }
: { username: "unknown user" };
}
return results;
} catch (error) {
console.error(error.message);
return [];
}
},
count: async function (clause = {}) {
try {
const count = await prisma.event_logs.count({

View File

@ -2,11 +2,9 @@
CREATE TABLE "event_logs" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"event" TEXT NOT NULL,
"description" TEXT,
"metadata" TEXT,
"userId" INTEGER,
"occurredAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"ipAddress" TEXT
"occurredAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateIndex

View File

@ -135,11 +135,9 @@ model cache_data {
model event_logs {
id Int @id @default(autoincrement())
event String
description String?
metadata String?
userId Int?
occurredAt DateTime @default(now())
ipAddress String?
@@index([userId])
@@index([event])
}