LOGS ARE GOOD
This commit is contained in:
parent
70f2acc21e
commit
e5fca7a476
9 changed files with 198 additions and 52 deletions
107
config.ts
107
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<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];
|
||||
}
|
||||
|
|
|
@ -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> = {
|
||||
|
|
14
db/mod.ts
14
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<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
2
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");
|
||||
|
|
|
@ -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
81
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<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
19
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)] });
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue