Configuration pedantry

This commit is contained in:
Daniel Flanagan 2022-11-10 10:25:35 -06:00
parent e5fca7a476
commit 773439d2e0
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
4 changed files with 62 additions and 21 deletions

View file

@ -9,6 +9,7 @@ export interface LogConfig {
export interface PostgresConfig { export interface PostgresConfig {
url: string; url: string;
poolSize: number;
} }
export interface MailgunConfig { export interface MailgunConfig {
@ -28,8 +29,12 @@ function truthyEnv(key: string): boolean {
["true", "1"]; ["true", "1"];
} }
function isLogLevelName(s: string): s is LevelName { function toLogLevelName(s: string): LevelName | undefined {
return s in LogLevels; if (s in LogLevels) {
return s as LevelName;
} else {
return undefined;
}
} }
export let config: Readonly<Config>; export let config: Readonly<Config>;
@ -45,7 +50,7 @@ export function reload(): [
((logger: Logger) => void)[], ((logger: Logger) => void)[],
] { ] {
// since we want configuration to be setup before any logging, lets save logs here until the logger is setup // since we want configuration to be setup before any logging, lets save logs here until the logger is setup
const logCalls = []; const logCalls: ((logger: Logger) => void)[] = [];
const envOrWarn = (key: string, fallback: string): string => { const envOrWarn = (key: string, fallback: string): string => {
const val = Deno.env.get(key); const val = Deno.env.get(key);
@ -57,28 +62,49 @@ export function reload(): [
return val || fallback; return val || fallback;
}; };
const desiredLogLevel = envOrWarn("LOG_LEVEL", "INFO").toUpperCase(); const envToIntOrWarn = (key: string, fallback: number): number => {
if (!isLogLevelName(desiredLogLevel)) { const ev = envOrWarn(key, fallback.toString());
let result = parseInt(ev);
if (isNaN(result)) {
logCalls.push((logger: Logger) => logCalls.push((logger: Logger) =>
logger.warning( logger.warning(
`Specified LOG_LEVEL '${desiredLogLevel}' is invalid. Falling back to INFO`, `Specified ${key} '${ev}' is not a valid integer. Falling back to ${fallback}.`,
) )
); );
result = fallback;
} }
return result;
};
const logLevel: LevelName = isLogLevelName(desiredLogLevel) const envToEnumOrWarn = <T extends { toString(self: T): string }>(
? desiredLogLevel key: string,
: "INFO"; converter: (s: string) => T | undefined,
fallback: T,
): T => {
const ev = envOrWarn(key, `${fallback}`);
const result = converter(ev);
if (result) {
return result;
} else {
logCalls.push((logger: Logger) =>
logger.warning(
`Specified ${key} '${ev}' is invalid. Falling back to ${fallback}`,
)
);
return fallback;
}
};
const config: Config = { const config: Config = {
log: { log: {
consoleLevelName: logLevel, consoleLevelName: envToEnumOrWarn("LOG_LEVEL", toLogLevelName, "INFO"),
}, },
postgres: { postgres: {
url: envOrWarn( url: envOrWarn(
"POSTGRES_URL", "POSTGRES_URL",
"postgresql://postgres:@127.0.0.1:5432/lyricscreen", "postgresql://postgres:@127.0.0.1:5432/lyricscreen",
), ),
poolSize: envToIntOrWarn("POSTGRES_POOL_SIZE", 3),
}, },
mailgun: {}, mailgun: {},
isDevelopmentMode: truthyEnv("DEVELOPMENT_MODE"), isDevelopmentMode: truthyEnv("DEVELOPMENT_MODE"),

View file

@ -35,6 +35,18 @@ let pool: Pool;
export function initDatabaseConnectionPool({ url }: PostgresConfig) { export function initDatabaseConnectionPool({ url }: PostgresConfig) {
pool = new Pool(url, 3, true); pool = new Pool(url, 3, true);
testDbConnection();
}
export async function testDbConnection(): Promise<boolean> {
try {
await dbOp((conn) => conn.queryObject("select 1"));
log.info("Successfully connected to database");
return true;
} catch (e) {
log.critical("Failed to connect to database:", e);
return false;
}
} }
type QueryResult<T> = { rows: T[] } | null; type QueryResult<T> = { rows: T[] } | null;
@ -383,6 +395,8 @@ export async function getUser(
} }
} }
// TODO: refresh token?
export async function getUserFromNonExpiredLoginToken( export async function getUserFromNonExpiredLoginToken(
token: TokenDigest, token: TokenDigest,
): Promise<User> { ): Promise<User> {

12
log.ts
View file

@ -42,19 +42,21 @@ class CustomConsoleHandler extends log.handlers.ConsoleHandler {
} }
} }
export function setupLoggers(config: LogConfig) { export function setupLoggers(config: LogConfig, redact?: RegExp) {
// TODO: support for colors or nah? // TODO: check support for colors and adjust as needed
log.setup({ log.setup({
handlers: { handlers: {
console: new CustomConsoleHandler( console: new CustomConsoleHandler(
config.consoleLevelName, config.consoleLevelName,
{ {
formatter: ({ level, datetime, msg, args }: log.LogRecord) => { formatter: ({ level, datetime, msg, args }: log.LogRecord) => {
// TODO: use a replacer for redacting secrets? let fullMessage = format(
const fullMessage = format(
msg, msg,
args, ...args,
); );
if (redact) {
fullMessage = fullMessage.replaceAll(redact, "<REDACTED>");
}
return `\x1b[m[\x1b[${levelColors[level] || ""}m${ return `\x1b[m[\x1b[${levelColors[level] || ""}m${
short[level] || "UNK" short[level] || "UNK"
}\x1b[m] \x1b[90m${datetime.toISOString()}\x1b[m \x1b[${ }\x1b[m] \x1b[90m${datetime.toISOString()}\x1b[m \x1b[${

View file

@ -13,7 +13,6 @@ export const handler: Handlers<UserId | RegistrationError | null> = {
const formData = (await request.formData()); const formData = (await request.formData());
const username = formData.get("username"); const username = formData.get("username");
const password = formData.get("password"); const password = formData.get("password");
// TODO: verify that username conforms to some regex? no spaces?
if (!username) { if (!username) {
return await context.render({ message: "no username provided" }); return await context.render({ message: "no username provided" });
} }