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, LogLevels,
} from "https://deno.land/std@0.159.0/log/mod.ts"; } from "https://deno.land/std@0.159.0/log/mod.ts";
export interface Config { export interface LogConfig {
log: { consoleLevelName: LevelName;
consoleLevelName: LevelName;
};
postgres: {
url: string;
};
mailgun: {
apiKey?: string;
domain?: string;
};
} }
function envOrWarn(key: string, fallback: string): string { export interface PostgresConfig {
const val = Deno.env.get(key); url: string;
if (!val) console.warn(`${key} is not set! Using fallback: ${fallback}`); }
return val || fallback;
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 { function isLogLevelName(s: string): s is LevelName {
return s in LogLevels; return s in LogLevels;
} }
const desiredLogLevel = envOrWarn("LOG_LEVEL", "INFO").toUpperCase(); export let config: Readonly<Config>;
if (!isLogLevelName(desiredLogLevel)) {
console.warn( type Logger = { warning: (s: string, ...opts: unknown[]) => void };
`Desired LOG_LEVEL of '${desiredLogLevel}' is invalid. Falling back to 'INFO'`,
); export function setAll(newConfig: Config) {
config = newConfig;
} }
const logLevel: LevelName = isLogLevelName(desiredLogLevel) export function reload(): [
? desiredLogLevel Config,
: "INFO"; ((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 = { const envOrWarn = (key: string, fallback: string): string => {
log: { const val = Deno.env.get(key);
consoleLevelName: logLevel, if (!val) {
}, logCalls.push((logger: Logger) =>
postgres: { logger.warning(`${key} is not set! Using fallback: ${fallback}`)
url: envOrWarn( );
"POSTGRES_URL", }
"postgresql://postgres:@127.0.0.1:5432/lyricscreen", return val || fallback;
), };
},
mailgun: {}, 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()"; 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; select (lpad(to_hex(floor(extract(epoch from clock_timestamp()) * 1000)::bigint), 12, '0') || encode(gen_random_bytes(10), 'hex'))::uuid;
$$ language sql $$ language sql
`, `,
`
`,
]; ];
const tables: Record<string, TableSpec> = { const tables: Record<string, TableSpec> = {

View file

@ -10,10 +10,10 @@ import {
type QueryArrayResult, type QueryArrayResult,
type QueryObjectResult, type QueryObjectResult,
} from "https://deno.land/x/postgres@v0.16.1/query/query.ts?s=QueryArguments"; } 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 * as base64 from "$std/encoding/base64.ts";
import { log } from "@/log.ts"; import { log } from "@/log.ts";
import { type PostgresConfig } from "@/config.ts";
import { import {
type Display, type Display,
type Note, type Note,
@ -31,7 +31,11 @@ import { sha256 } from "https://denopkg.com/chiefbiiko/sha256@v1.0.0/mod.ts";
export { PostgresError }; export { PostgresError };
export { type QueryObjectResult }; 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; type QueryResult<T> = { rows: T[] } | null;
@ -79,14 +83,14 @@ export async function dbOp<T>(
try { try {
result = await op(connection); result = await op(connection);
} catch (err) { } catch (err) {
log.error("Error querying database:", err, { ...err }); log.error("Error querying database:", err);
exception = err; exception = err;
} finally { } finally {
connection.release(); connection.release();
} }
} catch (err) { } catch (err) {
exception = err; exception = err;
log.error("Error connecting to database:", err); log.critical("Error connecting to database:", err);
} }
if (exception != null) throw exception; if (exception != null) throw exception;
if (result == null) { if (result == null) {
@ -282,7 +286,7 @@ export async function createUser(
), ),
); );
await createTeam({ await createTeam({
team: { displayName: `${username}'s First Team` }, team: { displayName: `${username}'s Team` },
creator: user, creator: user,
}, transaction); }, transaction);

2
dev.ts
View file

@ -1,5 +1,7 @@
#!/usr/bin/env -S deno run -A --watch=static/,routes/ #!/usr/bin/env -S deno run -A --watch=static/,routes/
Deno.env.set("DEVELOPMENT_MODE", "1");
import dev from "$fresh/dev.ts"; import dev from "$fresh/dev.ts";
await dev(import.meta.url, "./main.ts"); await dev(import.meta.url, "./main.ts");

View file

@ -1,7 +1,7 @@
{ {
"imports": { "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/", "$fresh/": "https://raw.githubusercontent.com/lytedev/fresh/v1.1.2-df/",
"$freshrel/": "../fresh/", "$freshrel/": "../fresh/",
"preact": "https://esm.sh/preact@10.11.0", "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 * 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 * 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({ log.setup({
handlers: { handlers: {
console: new log.handlers.ConsoleHandler(config.log.consoleLevelName, { console: new CustomConsoleHandler(
formatter: `{datetime} {levelName} {msg}`, 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: { loggers: {
default: { 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 twindPlugin from "$fresh/plugins/twind.ts";
import twindConfig from "@/twind.config.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)] }); await start(manifest, { plugins: [twindPlugin(twindConfig)] });

View file

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

View file

@ -9,6 +9,9 @@ interface TeamPageProps {
export const handler: Handlers<TeamPageProps> = { export const handler: Handlers<TeamPageProps> = {
async GET(request, context) { 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; const { id } = context.params;
console.debug({ request, context }); console.debug({ request, context });
try { try {