mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 06:26:44 -06:00
feat: add Docker containerization with multi-stage build
Includes .dockerignore, Dockerfile with cargo-chef caching, and Justfile commands for building/running containerized app. Updates console-logger to support both JSON and pretty-printed logs based on LOG_JSON env var.
This commit is contained in:
+35
-20
@@ -1,6 +1,3 @@
|
||||
// Patch console methods to output structured JSON logs
|
||||
// This runs before the Bun server starts to ensure all console output is formatted
|
||||
|
||||
const originalConsole = {
|
||||
log: console.log,
|
||||
error: console.error,
|
||||
@@ -9,23 +6,41 @@ const originalConsole = {
|
||||
debug: console.debug,
|
||||
};
|
||||
|
||||
const useJson = process.env.LOG_JSON === "true" || process.env.LOG_JSON === "1";
|
||||
|
||||
function formatLog(level, args) {
|
||||
const message = args.map(arg =>
|
||||
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
||||
).join(' ');
|
||||
|
||||
const logEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level: level,
|
||||
message: message,
|
||||
target: 'bun',
|
||||
};
|
||||
|
||||
originalConsole.log(JSON.stringify(logEntry));
|
||||
const message = args
|
||||
.map((arg) => (typeof arg === "object" ? JSON.stringify(arg) : String(arg)))
|
||||
.join(" ");
|
||||
|
||||
if (useJson) {
|
||||
const logEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level: level,
|
||||
message: message,
|
||||
target: "bun",
|
||||
};
|
||||
originalConsole.log(JSON.stringify(logEntry));
|
||||
} else {
|
||||
const timestamp = new Date().toISOString().split("T")[1].slice(0, 12);
|
||||
const levelColors = {
|
||||
debug: "\x1b[36m", // cyan
|
||||
info: "\x1b[32m", // green
|
||||
warn: "\x1b[33m", // yellow
|
||||
error: "\x1b[31m", // red
|
||||
};
|
||||
const color = levelColors[level] || "";
|
||||
const reset = "\x1b[0m";
|
||||
const gray = "\x1b[90m";
|
||||
|
||||
originalConsole.log(
|
||||
`${gray}${timestamp}${reset} ${color}${level.toUpperCase().padEnd(5)}${reset} ${gray}bun${reset}: ${message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log = (...args) => formatLog('info', args);
|
||||
console.info = (...args) => formatLog('info', args);
|
||||
console.warn = (...args) => formatLog('warn', args);
|
||||
console.error = (...args) => formatLog('error', args);
|
||||
console.debug = (...args) => formatLog('debug', args);
|
||||
console.log = (...args) => formatLog("info", args);
|
||||
console.info = (...args) => formatLog("info", args);
|
||||
console.warn = (...args) => formatLog("warn", args);
|
||||
console.error = (...args) => formatLog("error", args);
|
||||
console.debug = (...args) => formatLog("debug", args);
|
||||
|
||||
@@ -3,13 +3,11 @@ import { dev } from "$app/environment";
|
||||
import { initLogger } from "$lib/logger";
|
||||
import { getLogger } from "@logtape/logtape";
|
||||
|
||||
// Initialize logger on server startup
|
||||
await initLogger();
|
||||
|
||||
const logger = getLogger(["ssr", "error"]);
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// Handle DevTools request silently to prevent console.log spam
|
||||
if (
|
||||
dev &&
|
||||
event.url.pathname === "/.well-known/appspecific/com.chrome.devtools.json"
|
||||
@@ -26,7 +24,6 @@ export const handleError: HandleServerError = async ({
|
||||
status,
|
||||
message,
|
||||
}) => {
|
||||
// Use structured logging via LogTape instead of console.error
|
||||
logger.error(message, {
|
||||
status,
|
||||
method: event.request.method,
|
||||
|
||||
@@ -3,20 +3,11 @@ import { env } from "$env/dynamic/private";
|
||||
|
||||
const logger = getLogger(["ssr", "lib", "api"]);
|
||||
|
||||
// Compute upstream configuration once at module load
|
||||
const upstreamUrl = env.UPSTREAM_URL;
|
||||
const isUnixSocket =
|
||||
upstreamUrl?.startsWith("/") || upstreamUrl?.startsWith("./");
|
||||
const baseUrl = isUnixSocket ? "http://localhost" : upstreamUrl;
|
||||
|
||||
/**
|
||||
* Fetch utility for calling the Rust backend API.
|
||||
* Automatically prefixes requests with the upstream URL from environment.
|
||||
* Supports both HTTP URLs and Unix socket paths.
|
||||
*
|
||||
* Connection pooling and keep-alive are handled automatically by Bun.
|
||||
* Default timeout is 30 seconds unless overridden via init.signal.
|
||||
*/
|
||||
export async function apiFetch<T>(
|
||||
path: string,
|
||||
init?: RequestInit,
|
||||
@@ -29,10 +20,8 @@ export async function apiFetch<T>(
|
||||
const url = `${baseUrl}${path}`;
|
||||
const method = init?.method ?? "GET";
|
||||
|
||||
// Build fetch options with 30s default timeout and unix socket support
|
||||
const fetchOptions: RequestInit & { unix?: string } = {
|
||||
...init,
|
||||
// Respect caller-provided signal, otherwise default to 30s timeout
|
||||
signal: init?.signal ?? AbortSignal.timeout(30_000),
|
||||
};
|
||||
|
||||
|
||||
@@ -195,7 +195,6 @@
|
||||
float closestRad = 0.0;
|
||||
float pointOpacity = 0.0;
|
||||
|
||||
// Check 9 neighboring grid points
|
||||
for (float dx = -1.0; dx <= 1.0; dx += 1.0) {
|
||||
for (float dy = -1.0; dy <= 1.0; dy += 1.0) {
|
||||
vec2 testGrid = gridCoord + vec2(dx * spacing, dy * spacing);
|
||||
@@ -296,7 +295,6 @@
|
||||
|
||||
const { gl, program } = context;
|
||||
|
||||
// Setup fullscreen quad geometry
|
||||
const positionBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
gl.bufferData(
|
||||
@@ -309,7 +307,6 @@
|
||||
gl.enableVertexAttribArray(positionLocation);
|
||||
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
// Setup uniform manager
|
||||
const uniforms = new UniformManager(gl, program, [
|
||||
"u_resolution",
|
||||
"u_time",
|
||||
@@ -335,7 +332,6 @@
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
// Set static uniforms
|
||||
uniforms.setStatic({
|
||||
u_seed: Math.random() * 1000,
|
||||
u_dpr: dpr,
|
||||
|
||||
@@ -8,15 +8,6 @@ interface RailwayLogEntry {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom formatter that outputs Railway-compatible JSON logs.
|
||||
* Format: { timestamp, level, message, target, ...attributes }
|
||||
*
|
||||
* The target field is constructed from the logger category:
|
||||
* - ["ssr"] -> "ssr"
|
||||
* - ["ssr", "routes"] -> "ssr:routes"
|
||||
* - ["ssr", "api", "auth"] -> "ssr:api:auth"
|
||||
*/
|
||||
function railwayFormatter(record: LogRecord): string {
|
||||
const entry: RailwayLogEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -25,7 +16,6 @@ function railwayFormatter(record: LogRecord): string {
|
||||
target: record.category.join(":"),
|
||||
};
|
||||
|
||||
// Flatten properties to root level (custom attributes)
|
||||
if (record.properties && Object.keys(record.properties).length > 0) {
|
||||
Object.assign(entry, record.properties);
|
||||
}
|
||||
@@ -33,18 +23,12 @@ function railwayFormatter(record: LogRecord): string {
|
||||
return JSON.stringify(entry) + "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize LogTape with Railway-compatible JSON logging.
|
||||
* Only outputs logs when LOG_JSON=true or LOG_JSON=1 is set.
|
||||
* Safe to call multiple times (idempotent - will silently skip if already configured).
|
||||
*/
|
||||
export async function initLogger() {
|
||||
const useJsonLogs =
|
||||
process.env.LOG_JSON === "true" || process.env.LOG_JSON === "1";
|
||||
|
||||
try {
|
||||
if (!useJsonLogs) {
|
||||
// In development, use default console logging with nice formatting
|
||||
await configure({
|
||||
sinks: {
|
||||
console: getConsoleSink(),
|
||||
@@ -66,7 +50,6 @@ export async function initLogger() {
|
||||
return;
|
||||
}
|
||||
|
||||
// In production/JSON mode, use Railway-compatible JSON formatter
|
||||
await configure({
|
||||
sinks: {
|
||||
json: (record: LogRecord) => {
|
||||
@@ -75,13 +58,11 @@ export async function initLogger() {
|
||||
},
|
||||
filters: {},
|
||||
loggers: [
|
||||
// Meta logger for LogTape's internal messages
|
||||
{
|
||||
category: ["logtape", "meta"],
|
||||
lowestLevel: "warning",
|
||||
sinks: ["json"],
|
||||
},
|
||||
// SSR application logs
|
||||
{
|
||||
category: ["ssr"],
|
||||
lowestLevel: "info",
|
||||
@@ -90,7 +71,6 @@ export async function initLogger() {
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
// Already configured (HMR in dev mode), silently ignore
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("Already configured")
|
||||
|
||||
@@ -8,17 +8,12 @@
|
||||
</script>
|
||||
|
||||
<AppWrapper class="overflow-x-hidden font-schibsted">
|
||||
<!-- Top Navigation Bar -->
|
||||
<div class="flex w-full justify-end items-center pt-5 px-6 pb-9">
|
||||
<!-- <div class="flex gap-4 items-center"></div> -->
|
||||
</div>
|
||||
<div class="flex w-full justify-end items-center pt-5 px-6 pb-9"></div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex items-center flex-col">
|
||||
<div
|
||||
class="max-w-2xl mx-6 border-b border-zinc-700 divide-y divide-zinc-700"
|
||||
>
|
||||
<!-- Name & Occupation -->
|
||||
<div class="flex flex-col pb-4">
|
||||
<span class="text-3xl font-bold text-white">Ryan Walters,</span>
|
||||
<span class="text-2xl font-normal text-zinc-400">
|
||||
|
||||
@@ -9,9 +9,6 @@ interface RailwayLogEntry {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Railway-compatible JSON formatter for Vite logs
|
||||
*/
|
||||
function railwayFormatter(record: LogRecord): string {
|
||||
const entry: RailwayLogEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -20,7 +17,6 @@ function railwayFormatter(record: LogRecord): string {
|
||||
target: "vite",
|
||||
};
|
||||
|
||||
// Flatten properties to root level
|
||||
if (record.properties && Object.keys(record.properties).length > 0) {
|
||||
Object.assign(entry, record.properties);
|
||||
}
|
||||
@@ -28,7 +24,6 @@ function railwayFormatter(record: LogRecord): string {
|
||||
return JSON.stringify(entry) + "\n";
|
||||
}
|
||||
|
||||
// Strip ANSI escape codes from strings
|
||||
function stripAnsi(str: string): string {
|
||||
return str.replace(/\u001b\[[0-9;]*m/g, "").trim();
|
||||
}
|
||||
@@ -37,14 +32,12 @@ export function jsonLogger(): Plugin {
|
||||
const useJsonLogs =
|
||||
process.env.LOG_JSON === "true" || process.env.LOG_JSON === "1";
|
||||
|
||||
// If JSON logging is disabled, return a minimal plugin that does nothing
|
||||
if (!useJsonLogs) {
|
||||
return {
|
||||
name: "vite-plugin-json-logger",
|
||||
};
|
||||
}
|
||||
|
||||
// Configure LogTape for Vite plugin logging
|
||||
let loggerConfigured = false;
|
||||
const configureLogger = async () => {
|
||||
if (loggerConfigured) return;
|
||||
@@ -56,7 +49,6 @@ export function jsonLogger(): Plugin {
|
||||
},
|
||||
filters: {},
|
||||
loggers: [
|
||||
// Suppress LogTape meta logger info messages
|
||||
{
|
||||
category: ["logtape", "meta"],
|
||||
lowestLevel: "warning",
|
||||
@@ -86,7 +78,6 @@ export function jsonLogger(): Plugin {
|
||||
customLogger: {
|
||||
info(msg: string) {
|
||||
const cleaned = stripAnsi(msg);
|
||||
// Filter out noise
|
||||
if (
|
||||
!cleaned ||
|
||||
ignoredMessages.has(cleaned) ||
|
||||
@@ -108,9 +99,7 @@ export function jsonLogger(): Plugin {
|
||||
logger.error(cleaned);
|
||||
}
|
||||
},
|
||||
clearScreen() {
|
||||
// No-op since clearScreen is already false
|
||||
},
|
||||
clearScreen() {},
|
||||
hasErrorLogged() {
|
||||
return false;
|
||||
},
|
||||
@@ -126,7 +115,6 @@ export function jsonLogger(): Plugin {
|
||||
server = s;
|
||||
const logger = getLogger(["vite"]);
|
||||
|
||||
// Override the default URL printing
|
||||
const originalPrintUrls = server.printUrls;
|
||||
server.printUrls = () => {
|
||||
const urls = server.resolvedUrls;
|
||||
@@ -138,7 +126,6 @@ export function jsonLogger(): Plugin {
|
||||
}
|
||||
};
|
||||
|
||||
// Listen to server events
|
||||
server.httpServer?.once("listening", () => {
|
||||
logger.info("server listening");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user