LOGS ARE GOOD

This commit is contained in:
Daniel Flanagan 2022-11-09 16:55:27 -06:00
parent 70f2acc21e
commit e5fca7a476
Signed by: lytedev
GPG Key ID: 5B2020A0F9921EF4
9 changed files with 198 additions and 52 deletions

107
config.ts
View File

@ -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<Config>;
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];
}

View File

@ -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<string, TableSpec> = {

View File

@ -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<T> = { rows: T[] } | null;
@ -79,14 +83,14 @@ export async function dbOp<T>(
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);

2
dev.ts
View File

@ -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");

View File

@ -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",

81
log.ts
View File

@ -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<number, string> = {
10: "DBG",
20: "INF",
30: "WRN",
40: "ERR",
50: "CRT",
};
const levelColors: Record<number, string> = {
10: "90",
20: "34",
30: "33",
40: "31",
50: "35",
};
const msgColors: Record<number, string> = {
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<number> {
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");
}

19
main.ts
View File

@ -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)] });

View File

@ -34,7 +34,7 @@ function Dashboard({ teams, user }: DashboardProps) {
Hello, {(user.displayName || user.username).trim()}!
</h2>
<h3 class="text-lg">
Which team are we working with today?
Here are your teams:
</h3>
<ul>
{teams.map((team) => (

View File

@ -9,6 +9,9 @@ interface TeamPageProps {
export const handler: Handlers<TeamPageProps> = {
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 {