diff --git a/drizzle/0002_aromatic_nightcrawler.sql b/drizzle/0002_aromatic_nightcrawler.sql new file mode 100644 index 0000000..768f7c8 --- /dev/null +++ b/drizzle/0002_aromatic_nightcrawler.sql @@ -0,0 +1,2 @@ +ALTER TABLE "logs" ADD COLUMN "level" text DEFAULT 'info' NOT NULL;--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "log_level_idx" ON "logs" ("level"); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index ddb8a0b..92c46c5 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -67,9 +67,7 @@ "indexes": { "log_created_at_idx": { "name": "log_created_at_idx", - "columns": [ - "created_at" - ], + "columns": ["created_at"], "isUnique": false } }, @@ -78,12 +76,8 @@ "name": "logs_service_id_services_id_fk", "tableFrom": "logs", "tableTo": "services", - "columnsFrom": [ - "service_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["service_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -135,9 +129,7 @@ "indexes": { "service_created_at_idx": { "name": "service_created_at_idx", - "columns": [ - "created_at" - ], + "columns": ["created_at"], "isUnique": false } }, @@ -152,4 +144,4 @@ "tables": {}, "columns": {} } -} \ No newline at end of file +} diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json index bb9a4c8..afe53e1 100644 --- a/drizzle/meta/0001_snapshot.json +++ b/drizzle/meta/0001_snapshot.json @@ -67,9 +67,7 @@ "indexes": { "log_created_at_idx": { "name": "log_created_at_idx", - "columns": [ - "created_at" - ], + "columns": ["created_at"], "isUnique": false } }, @@ -78,12 +76,8 @@ "name": "logs_service_id_services_id_fk", "tableFrom": "logs", "tableTo": "services", - "columnsFrom": [ - "service_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["service_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -135,9 +129,7 @@ "indexes": { "service_created_at_idx": { "name": "service_created_at_idx", - "columns": [ - "created_at" - ], + "columns": ["created_at"], "isUnique": false } }, @@ -152,4 +144,4 @@ "tables": {}, "columns": {} } -} \ No newline at end of file +} diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..14ba2ea --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,171 @@ +{ + "id": "ce2127be-1277-4db7-b4c3-789c393f369a", + "prevId": "19b9ab6a-21fa-4bb0-91c2-2460725df8c5", + "version": "5", + "dialect": "pg", + "tables": { + "logs": { + "name": "logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "service_id": { + "name": "service_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "lookup_filter_value": { + "name": "lookup_filter_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_persisted": { + "name": "is_persisted", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "log_created_at_idx": { + "name": "log_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "log_level_idx": { + "name": "log_level_idx", + "columns": [ + "level" + ], + "isUnique": false + } + }, + "foreignKeys": { + "logs_service_id_services_id_fk": { + "name": "logs_service_id_services_id_fk", + "tableFrom": "logs", + "tableTo": "services", + "columnsFrom": [ + "service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "services": { + "name": "services", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_persisted": { + "name": "is_persisted", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "service_created_at_idx": { + "name": "service_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 21c25a4..c9c5c34 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1688826426429, "tag": "0001_clean_blindfold", "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1704635877860, + "tag": "0002_aromatic_nightcrawler", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3e7a134..aead7f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "simple-logging-server", - "version": "2.1.2", + "version": "2.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "simple-logging-server", - "version": "2.1.2", + "version": "2.2.0", "license": "MIT", "dependencies": { "@fastify/rate-limit": "^9.1.0", @@ -23,7 +23,7 @@ "@types/node": "^20.10.6", "@typescript-eslint/eslint-plugin": "^6.18.0", "@typescript-eslint/parser": "^6.18.0", - "drizzle-kit": "^0.20.10", + "drizzle-kit": "^0.20.9", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.2", @@ -2088,9 +2088,9 @@ } }, "node_modules/drizzle-kit": { - "version": "0.20.10", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.20.10.tgz", - "integrity": "sha512-GoBmfAWrZiX2ZHMGlVg7w34bnJw1gbQIzhGr1cROK6+0eikhFNSR4a4G8jtD3FO5JZKbbWHuNA5YFDJu6Us0Tw==", + "version": "0.20.9", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.20.9.tgz", + "integrity": "sha512-5oIbPFdfEEfzVSOB3MWGt70VSHv6W7qMAWCJ5xc6W1BxgGASipxuAuyXD59fx9S6QYTNNnuSuQFoIdnNTRWY2A==", "dev": true, "dependencies": { "@drizzle-team/studio": "^0.0.37", @@ -6247,9 +6247,9 @@ } }, "drizzle-kit": { - "version": "0.20.10", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.20.10.tgz", - "integrity": "sha512-GoBmfAWrZiX2ZHMGlVg7w34bnJw1gbQIzhGr1cROK6+0eikhFNSR4a4G8jtD3FO5JZKbbWHuNA5YFDJu6Us0Tw==", + "version": "0.20.9", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.20.9.tgz", + "integrity": "sha512-5oIbPFdfEEfzVSOB3MWGt70VSHv6W7qMAWCJ5xc6W1BxgGASipxuAuyXD59fx9S6QYTNNnuSuQFoIdnNTRWY2A==", "dev": true, "requires": { "@drizzle-team/studio": "^0.0.37", diff --git a/package.json b/package.json index 4bc1a16..8096496 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-logging-server", - "version": "2.1.2", + "version": "2.2.0", "description": "This is a simple API for logging messages", "private": true, "main": "dist/index.js", @@ -24,7 +24,7 @@ "@types/node": "^20.10.6", "@typescript-eslint/eslint-plugin": "^6.18.0", "@typescript-eslint/parser": "^6.18.0", - "drizzle-kit": "^0.20.10", + "drizzle-kit": "^0.20.9", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.2", diff --git a/src/components/logging/logging.controller.ts b/src/components/logging/logging.controller.ts index f57c2d7..5809005 100644 --- a/src/components/logging/logging.controller.ts +++ b/src/components/logging/logging.controller.ts @@ -14,7 +14,7 @@ export async function createLogForServiceHandler( Headers: TXAppServiceIdHeaderSchema; Body: CreateLogInput; }>, - reply: FastifyReply + reply: FastifyReply, ) { if (env.FREEZE_DB_WRITES) { return reply.code(503).send({ success: false, message: "Database writes are currently frozen" }); @@ -41,7 +41,7 @@ export async function getLogsForServiceHandler( Headers: TXAppServiceIdHeaderSchema; Querystring: TGetLogsSearchParamsInput; }>, - reply: FastifyReply + reply: FastifyReply, ) { const serviceId = request.headers["x-app-service-id"]; @@ -53,6 +53,7 @@ export async function getLogsForServiceHandler( environment: request.query.environment, limit: request.query.page_size, skip: (request.query.page - 1) * request.query.page_size, + level: request.query.level, }); reply.code(200).send(logs); } catch (error) { @@ -64,7 +65,7 @@ export async function cleanLogsForAllHandler( request: FastifyRequest<{ Headers: TXAppServiceIdHeaderSchema; }>, - reply: FastifyReply + reply: FastifyReply, ) { if (env.FREEZE_DB_WRITES) { return reply.code(503).send({ success: false, message: "Database writes are currently frozen", errors: [] }); @@ -93,6 +94,7 @@ export async function cleanLogsForAllHandler( environment: "production", lookupFilterValue: "app-admin-action", data: { client: client.name, ip }, + level: "info", }); reply.statusCode = 200; diff --git a/src/components/logging/logging.schema.ts b/src/components/logging/logging.schema.ts index 119df98..61a0c8e 100644 --- a/src/components/logging/logging.schema.ts +++ b/src/components/logging/logging.schema.ts @@ -6,6 +6,15 @@ const logCreateInput = { ip: z.string().nullable().optional(), lookupFilterValue: z.string().nullable().optional(), data: z.record(z.string(), z.any()).nullable(), + level: z + .preprocess( + (val) => { + if (val) return val; + return val; + }, + z.enum(["info", "warn", "error", "fatal"]), + ) + .default("info"), }; const CreateLogInputSchema = z.object({ ...logCreateInput, @@ -30,6 +39,16 @@ const getLogsQueryParamsInput = { sort: z.enum(["ASC", "DESC"]).default("DESC").optional(), page: z.coerce.number().min(1).optional().default(1), page_size: z.coerce.number().min(1).optional().default(50), + level: z + .preprocess( + (val) => { + if (val) return val; + return ["all"]; + }, + z.array(z.enum(["all", "info", "warn", "error", "fatal"])), + ) + .default(["all"]) + .optional(), }; const GetLogsSearchParamsSchema = z.object({ diff --git a/src/components/logging/logging.service.ts b/src/components/logging/logging.service.ts index 140d205..3fcc333 100644 --- a/src/components/logging/logging.service.ts +++ b/src/components/logging/logging.service.ts @@ -1,6 +1,6 @@ -import { and, eq, lt } from "drizzle-orm"; +import { and, eq, lt, inArray } from "drizzle-orm"; -import { CreateLogInput } from "./logging.schema"; +import { CreateLogInput, TGetLogsSearchParamsInput } from "./logging.schema"; import { db } from "../../config/db"; import { logs as logsTable } from "../../config/db/schema"; @@ -21,6 +21,7 @@ export async function createLog(data: CreateLogInput & { serviceId: string; isPe data: data.data || {}, isPersisted: data.isPersisted || false, lookupFilterValue: data.lookupFilterValue, + level: data.level, }) .returning({ id: logsTable.id, @@ -29,6 +30,7 @@ export async function createLog(data: CreateLogInput & { serviceId: string; isPe ip: logsTable.ip, lookupFilterValue: logsTable.lookupFilterValue, data: logsTable.data, + level: logsTable.level, serviceId: logsTable.serviceId, createdAt: logsTable.createdAt, @@ -42,6 +44,7 @@ export async function getLogs({ skip = 0, limit = 500, sortDirection = "desc", + level = ["all"], ...data }: { serviceId: string; @@ -50,7 +53,11 @@ export async function getLogs({ sortDirection?: "asc" | "desc"; limit?: number; skip?: number; + level: TGetLogsSearchParamsInput["level"]; }) { + const isSpecificLogLevel = level && !level.includes("all"); // searching for a specific log level + const levelsWithoutAll = level.filter((val) => val !== "all") as unknown as string[]; + const logs = await db.query.logs.findMany({ limit, offset: skip, @@ -60,6 +67,7 @@ export async function getLogs({ ...[eq(table.serviceId, data.serviceId)], ...(data.environment ? [eq(table.environment, data.environment)] : []), ...(data.lookupValue ? [eq(table.lookupFilterValue, data.lookupValue)] : []), + ...(isSpecificLogLevel ? [inArray(table.level, levelsWithoutAll)] : []), ), }); diff --git a/src/config/db/schema.ts b/src/config/db/schema.ts index 5c773a4..0d3b9be 100644 --- a/src/config/db/schema.ts +++ b/src/config/db/schema.ts @@ -13,14 +13,16 @@ export const logs = pgTable( serviceId: text("service_id") .notNull() .references(() => services.id, { onDelete: "cascade", onUpdate: "cascade" }), + level: text("level").default("info").notNull(), lookupFilterValue: text("lookup_filter_value"), isPersisted: boolean("is_persisted").notNull(), }, (table) => { return { createdAtIdx: index("log_created_at_idx").on(table.createdAt), + levelIdx: index("log_level_idx").on(table.level), }; - } + }, ); export const logRelations = relations(logs, ({ one }) => ({ @@ -44,7 +46,7 @@ export const services = pgTable( return { createdAtIdx: index("service_created_at_idx").on(table.createdAt), }; - } + }, ); export const serviceRelations = relations(services, ({ many }) => ({