From e5fca7a476b40a593d49e408570353b8f7023e78 Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Wed, 9 Nov 2022 16:55:27 -0600 Subject: [PATCH] LOGS ARE GOOD --- config.ts | 107 +++++++++++++++++++++++++++++-------------- db/migrations.ts | 20 ++++++-- db/mod.ts | 14 ++++-- dev.ts | 2 + import_map.json | 2 +- log.ts | 81 ++++++++++++++++++++++++++++++-- main.ts | 19 +++++++- routes/dashboard.tsx | 2 +- routes/team/[id].tsx | 3 ++ 9 files changed, 198 insertions(+), 52 deletions(-) diff --git a/config.ts b/config.ts index a2e0028..19560fc 100644 --- a/config.ts +++ b/config.ts @@ -3,49 +3,86 @@ import { LogLevels, } from "https://deno.land/std@0.159.0/log/mod.ts"; -export interface Config { - log: { - consoleLevelName: LevelName; - }; - postgres: { - url: string; - }; - mailgun: { - apiKey?: string; - domain?: string; - }; +export interface LogConfig { + consoleLevelName: LevelName; } -function envOrWarn(key: string, fallback: string): string { - const val = Deno.env.get(key); - if (!val) console.warn(`${key} is not set! Using fallback: ${fallback}`); - return val || fallback; +export interface PostgresConfig { + url: string; +} + +export interface MailgunConfig { + apiKey?: string; + domain?: string; +} + +export interface Config { + log: LogConfig; + postgres: PostgresConfig; + mailgun: MailgunConfig; + isDevelopmentMode: boolean; +} + +function truthyEnv(key: string): boolean { + return (Deno.env.get(key) || "").toString().toLowerCase().trim() in + ["true", "1"]; } function isLogLevelName(s: string): s is LevelName { return s in LogLevels; } -const desiredLogLevel = envOrWarn("LOG_LEVEL", "INFO").toUpperCase(); -if (!isLogLevelName(desiredLogLevel)) { - console.warn( - `Desired LOG_LEVEL of '${desiredLogLevel}' is invalid. Falling back to 'INFO'`, - ); +export let config: Readonly; + +type Logger = { warning: (s: string, ...opts: unknown[]) => void }; + +export function setAll(newConfig: Config) { + config = newConfig; } -const logLevel: LevelName = isLogLevelName(desiredLogLevel) - ? desiredLogLevel - : "INFO"; +export function reload(): [ + Config, + ((logger: Logger) => void)[], +] { + // since we want configuration to be setup before any logging, lets save logs here until the logger is setup + const logCalls = []; -export const config: Config = { - log: { - consoleLevelName: logLevel, - }, - postgres: { - url: envOrWarn( - "POSTGRES_URL", - "postgresql://postgres:@127.0.0.1:5432/lyricscreen", - ), - }, - mailgun: {}, -}; + const envOrWarn = (key: string, fallback: string): string => { + const val = Deno.env.get(key); + if (!val) { + logCalls.push((logger: Logger) => + logger.warning(`${key} is not set! Using fallback: ${fallback}`) + ); + } + return val || fallback; + }; + + const desiredLogLevel = envOrWarn("LOG_LEVEL", "INFO").toUpperCase(); + if (!isLogLevelName(desiredLogLevel)) { + logCalls.push((logger: Logger) => + logger.warning( + `Specified LOG_LEVEL '${desiredLogLevel}' is invalid. Falling back to INFO`, + ) + ); + } + + const logLevel: LevelName = isLogLevelName(desiredLogLevel) + ? desiredLogLevel + : "INFO"; + + const config: Config = { + log: { + consoleLevelName: logLevel, + }, + postgres: { + url: envOrWarn( + "POSTGRES_URL", + "postgresql://postgres:@127.0.0.1:5432/lyricscreen", + ), + }, + mailgun: {}, + isDevelopmentMode: truthyEnv("DEVELOPMENT_MODE"), + }; + + return [config, logCalls]; +} diff --git a/db/migrations.ts b/db/migrations.ts index 744b88e..cf8ca03 100644 --- a/db/migrations.ts +++ b/db/migrations.ts @@ -1,4 +1,20 @@ -import { createNote, createUser, queryArray } from "@/db/mod.ts"; +import { + createNote, + createUser, + initDatabaseConnectionPool, + queryArray, +} from "@/db/mod.ts"; +import { reload } from "@/config.ts"; +import { log, setupLoggers } from "@/log.ts"; + +const [config, configLoadLogCallbacks] = reload(); +setupLoggers(config.log); + +for (const f of configLoadLogCallbacks) { + f(log); +} + +initDatabaseConnectionPool(config.postgres); const id = "id uuid primary key default generate_ulid()"; @@ -26,8 +42,6 @@ const functions = [ select (lpad(to_hex(floor(extract(epoch from clock_timestamp()) * 1000)::bigint), 12, '0') || encode(gen_random_bytes(10), 'hex'))::uuid; $$ language sql `, - ` - `, ]; const tables: Record = { diff --git a/db/mod.ts b/db/mod.ts index 015327c..7d74ee7 100644 --- a/db/mod.ts +++ b/db/mod.ts @@ -10,10 +10,10 @@ import { type QueryArrayResult, type QueryObjectResult, } from "https://deno.land/x/postgres@v0.16.1/query/query.ts?s=QueryArguments"; -import { config } from "@/config.ts"; import * as base64 from "$std/encoding/base64.ts"; import { log } from "@/log.ts"; +import { type PostgresConfig } from "@/config.ts"; import { type Display, type Note, @@ -31,7 +31,11 @@ import { sha256 } from "https://denopkg.com/chiefbiiko/sha256@v1.0.0/mod.ts"; export { PostgresError }; export { type QueryObjectResult }; -const pool = new Pool(config.postgres.url, 3, true); +let pool: Pool; + +export function initDatabaseConnectionPool({ url }: PostgresConfig) { + pool = new Pool(url, 3, true); +} type QueryResult = { rows: T[] } | null; @@ -79,14 +83,14 @@ export async function dbOp( try { result = await op(connection); } catch (err) { - log.error("Error querying database:", err, { ...err }); + log.error("Error querying database:", err); exception = err; } finally { connection.release(); } } catch (err) { exception = err; - log.error("Error connecting to database:", err); + log.critical("Error connecting to database:", err); } if (exception != null) throw exception; if (result == null) { @@ -282,7 +286,7 @@ export async function createUser( ), ); await createTeam({ - team: { displayName: `${username}'s First Team` }, + team: { displayName: `${username}'s Team` }, creator: user, }, transaction); diff --git a/dev.ts b/dev.ts index 2d85d6c..dcd6f83 100755 --- a/dev.ts +++ b/dev.ts @@ -1,5 +1,7 @@ #!/usr/bin/env -S deno run -A --watch=static/,routes/ +Deno.env.set("DEVELOPMENT_MODE", "1"); + import dev from "$fresh/dev.ts"; await dev(import.meta.url, "./main.ts"); diff --git a/import_map.json b/import_map.json index 11431d0..b7b51bf 100644 --- a/import_map.json +++ b/import_map.json @@ -1,7 +1,7 @@ { "imports": { "@/": "./", - "$std/": "https://deno.land/std@0.158.0/", + "$std/": "https://deno.land/std@0.162.0/", "$fresh/": "https://raw.githubusercontent.com/lytedev/fresh/v1.1.2-df/", "$freshrel/": "../fresh/", "preact": "https://esm.sh/preact@10.11.0", diff --git a/log.ts b/log.ts index cbc21f8..18dbae4 100644 --- a/log.ts +++ b/log.ts @@ -1,14 +1,68 @@ -import { config } from "@/config.ts"; +import { type LogConfig } from "@/config.ts"; import * as log from "$std/log/mod.ts"; +import { format } from "https://deno.land/std@0.163.0/node/util.ts"; export * as log from "$std/log/mod.ts"; -export function setupLoggers() { +const short: Record = { + 10: "DBG", + 20: "INF", + 30: "WRN", + 40: "ERR", + 50: "CRT", +}; + +const levelColors: Record = { + 10: "90", + 20: "34", + 30: "33", + 40: "31", + 50: "35", +}; + +const msgColors: Record = { + 40: "31", + 50: "31", +}; + +class CustomConsoleHandler extends log.handlers.ConsoleHandler { + encoder: TextEncoder; + + constructor(levelName: log.LevelName, options: log.HandlerOptions) { + super(levelName, options); + this.encoder = new TextEncoder(); + } + + override log(msg: string): Promise { + const result = Deno.stdout.write( + this.encoder.encode(msg), + ); + Deno.stdout.write(new Uint8Array([0x0a])); + return result; + } +} + +export function setupLoggers(config: LogConfig) { + // TODO: support for colors or nah? log.setup({ handlers: { - console: new log.handlers.ConsoleHandler(config.log.consoleLevelName, { - formatter: `{datetime} {levelName} {msg}`, - }), + console: new CustomConsoleHandler( + config.consoleLevelName, + { + formatter: ({ level, datetime, msg, args }: log.LogRecord) => { + // TODO: use a replacer for redacting secrets? + const fullMessage = format( + msg, + args, + ); + return `\x1b[m[\x1b[${levelColors[level] || ""}m${ + short[level] || "UNK" + }\x1b[m] \x1b[90m${datetime.toISOString()}\x1b[m \x1b[${ + msgColors[level] || "" + }m${fullMessage}\x1b[m`; + }, + }, + ), }, loggers: { default: { @@ -21,4 +75,21 @@ export function setupLoggers() { }, }, }); + + /* + console.debug = log.debug; + console.info = log.info; + console.warn = log.warning; + console.error = log.error; + */ + + /* + log.debug("Debug Log"); + log.info("Info Log"); + log.warning("Warning Log"); + log.error("Error Log"); + log.critical("Critical Log"); + */ + + log.info("Logger setup complete"); } diff --git a/main.ts b/main.ts index e9dc21c..f93e23d 100644 --- a/main.ts +++ b/main.ts @@ -10,8 +10,23 @@ import manifest from "@/fresh.gen.ts"; import twindPlugin from "$fresh/plugins/twind.ts"; import twindConfig from "@/twind.config.ts"; -import { setupLoggers } from "@/log.ts"; +import { log, setupLoggers } from "@/log.ts"; +import { reload } from "@/config.ts"; +import { initDatabaseConnectionPool } from "@/db/mod.ts"; -setupLoggers(); +const [config, configLoadLogCallbacks] = reload(); +setupLoggers(config.log); + +for (const f of configLoadLogCallbacks) { + f(log); +} + +initDatabaseConnectionPool(config.postgres); + +console.log = log.info; +console.debug = log.debug; +console.info = log.info; +console.warn = log.warning; +console.error = log.error; await start(manifest, { plugins: [twindPlugin(twindConfig)] }); diff --git a/routes/dashboard.tsx b/routes/dashboard.tsx index 6e4e0ff..d76f754 100644 --- a/routes/dashboard.tsx +++ b/routes/dashboard.tsx @@ -34,7 +34,7 @@ function Dashboard({ teams, user }: DashboardProps) { Hello, {(user.displayName || user.username).trim()}!

- Which team are we working with today? + Here are your teams:

    {teams.map((team) => ( diff --git a/routes/team/[id].tsx b/routes/team/[id].tsx index 2a0bf99..f74a6ed 100644 --- a/routes/team/[id].tsx +++ b/routes/team/[id].tsx @@ -9,6 +9,9 @@ interface TeamPageProps { export const handler: Handlers = { async GET(request, context) { + // TODO: only allow logged-in users to view teams (and most resources!) + // TODO: only allow users that are a member of a team to view them + // NOTE: maybe teams can be public...? const { id } = context.params; console.debug({ request, context }); try {