LOGS ARE GOOD
This commit is contained in:
parent
70f2acc21e
commit
e5fca7a476
81
config.ts
81
config.ts
|
@ -3,41 +3,74 @@ 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(): [
|
||||||
|
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 = [];
|
||||||
|
|
||||||
|
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
|
? desiredLogLevel
|
||||||
: "INFO";
|
: "INFO";
|
||||||
|
|
||||||
export const config: Config = {
|
const config: Config = {
|
||||||
log: {
|
log: {
|
||||||
consoleLevelName: logLevel,
|
consoleLevelName: logLevel,
|
||||||
},
|
},
|
||||||
|
@ -48,4 +81,8 @@ export const config: Config = {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
mailgun: {},
|
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()";
|
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> = {
|
||||||
|
|
14
db/mod.ts
14
db/mod.ts
|
@ -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
2
dev.ts
|
@ -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");
|
||||||
|
|
|
@ -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
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 * 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
19
main.ts
|
@ -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)] });
|
||||||
|
|
|
@ -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) => (
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue