feat(logging): add log level env variable (#2299)

This commit is contained in:
Meier Lukas
2025-02-18 22:54:15 +01:00
committed by GitHub
parent 6420feee72
commit 67d48e11d7
31 changed files with 202 additions and 183 deletions

View File

@@ -1,8 +1,8 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
import { createBooleanSchema, createDurationSchema, shouldSkipEnvValidation } from "@homarr/common/env-validation";
import { supportedAuthProviders } from "@homarr/definitions";
import { createEnv } from "@homarr/env";
import { createBooleanSchema, createDurationSchema } from "@homarr/env/schemas";
const authProvidersSchema = z
.string()
@@ -22,8 +22,7 @@ const authProvidersSchema = z
)
.default("credentials");
const skipValidation = shouldSkipEnvValidation();
const authProviders = skipValidation ? [] : authProvidersSchema.parse(process.env.AUTH_PROVIDERS);
const authProviders = authProvidersSchema.safeParse(process.env.AUTH_PROVIDERS).data ?? [];
export const env = createEnv({
server: {
@@ -59,32 +58,5 @@ export const env = createEnv({
}
: {}),
},
client: {},
runtimeEnv: {
AUTH_LOGOUT_REDIRECT_URL: process.env.AUTH_LOGOUT_REDIRECT_URL,
AUTH_SESSION_EXPIRY_TIME: process.env.AUTH_SESSION_EXPIRY_TIME,
AUTH_PROVIDERS: process.env.AUTH_PROVIDERS,
AUTH_LDAP_BASE: process.env.AUTH_LDAP_BASE,
AUTH_LDAP_BIND_DN: process.env.AUTH_LDAP_BIND_DN,
AUTH_LDAP_BIND_PASSWORD: process.env.AUTH_LDAP_BIND_PASSWORD,
AUTH_LDAP_GROUP_CLASS: process.env.AUTH_LDAP_GROUP_CLASS,
AUTH_LDAP_GROUP_FILTER_EXTRA_ARG: process.env.AUTH_LDAP_GROUP_FILTER_EXTRA_ARG,
AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE: process.env.AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE,
AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE: process.env.AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE,
AUTH_LDAP_SEARCH_SCOPE: process.env.AUTH_LDAP_SEARCH_SCOPE,
AUTH_LDAP_URI: process.env.AUTH_LDAP_URI,
AUTH_OIDC_CLIENT_ID: process.env.AUTH_OIDC_CLIENT_ID,
AUTH_OIDC_CLIENT_NAME: process.env.AUTH_OIDC_CLIENT_NAME,
AUTH_OIDC_CLIENT_SECRET: process.env.AUTH_OIDC_CLIENT_SECRET,
AUTH_OIDC_ISSUER: process.env.AUTH_OIDC_ISSUER,
AUTH_OIDC_SCOPE_OVERWRITE: process.env.AUTH_OIDC_SCOPE_OVERWRITE,
AUTH_OIDC_GROUPS_ATTRIBUTE: process.env.AUTH_OIDC_GROUPS_ATTRIBUTE,
AUTH_LDAP_USERNAME_ATTRIBUTE: process.env.AUTH_LDAP_USERNAME_ATTRIBUTE,
AUTH_LDAP_USER_MAIL_ATTRIBUTE: process.env.AUTH_LDAP_USER_MAIL_ATTRIBUTE,
AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG: process.env.AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG,
AUTH_OIDC_AUTO_LOGIN: process.env.AUTH_OIDC_AUTO_LOGIN,
AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE: process.env.AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE,
},
skipValidation,
emptyStringAsUndefined: true,
experimental__runtimeEnv: process.env,
});

View File

@@ -28,9 +28,9 @@
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@t3-oss/env-nextjs": "^0.12.0",
"bcrypt": "^5.1.1",
"cookies": "^0.9.1",
"ldapts": "7.3.1",

View File

@@ -6,10 +6,11 @@ import { rootCertificates } from "node:tls";
import axios from "axios";
import { fetch } from "undici";
import { env } from "@homarr/common/env";
import { LoggingAgent } from "@homarr/common/server";
const getCertificateFolder = () => {
return process.env.NODE_ENV === "production"
return env.NODE_ENV === "production"
? path.join("/appdata", "trusted-certificates")
: process.env.LOCAL_CERTIFICATE_PATH;
};

View File

@@ -1,12 +1,14 @@
import { randomBytes } from "crypto";
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
import { shouldSkipEnvValidation } from "./src/env-validation";
import { createEnv } from "@homarr/env";
const errorSuffix = `, please generate a 64 character secret in hex format or use the following: "${randomBytes(32).toString("hex")}"`;
export const env = createEnv({
shared: {
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
},
server: {
SECRET_ENCRYPTION_KEY: z
.string({
@@ -24,7 +26,6 @@ export const env = createEnv({
},
runtimeEnv: {
SECRET_ENCRYPTION_KEY: process.env.SECRET_ENCRYPTION_KEY,
NODE_ENV: process.env.NODE_ENV,
},
skipValidation: shouldSkipEnvValidation(),
emptyStringAsUndefined: true,
});

View File

@@ -27,6 +27,7 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"dayjs": "^1.11.13",
"next": "15.1.7",

View File

@@ -1,7 +1,7 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
import { shouldSkipEnvValidation } from "@homarr/common/env-validation";
import { env as commonEnv } from "@homarr/common/env";
import { createEnv } from "@homarr/env";
const drivers = {
betterSqlite3: "better-sqlite3",
@@ -29,7 +29,7 @@ export const env = createEnv({
? {
DB_URL:
// Fallback to the default sqlite file path in production
process.env.NODE_ENV === "production" && isDriver("better-sqlite3")
commonEnv.NODE_ENV === "production" && isDriver("better-sqlite3")
? z.string().default("/appdata/db/db.sqlite")
: z.string().nonempty(),
}
@@ -49,18 +49,5 @@ export const env = createEnv({
}
: {}),
},
/**
* Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
*/
runtimeEnv: {
DB_DRIVER: process.env.DB_DRIVER,
DB_URL: process.env.DB_URL,
DB_HOST: process.env.DB_HOST,
DB_USER: process.env.DB_USER,
DB_PASSWORD: process.env.DB_PASSWORD,
DB_NAME: process.env.DB_NAME,
DB_PORT: process.env.DB_PORT,
},
skipValidation: shouldSkipEnvValidation(),
emptyStringAsUndefined: true,
experimental__runtimeEnv: process.env,
});

View File

@@ -40,11 +40,11 @@
"@auth/core": "^0.37.4",
"@homarr/common": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^7.17.0",
"@paralleldrive/cuid2": "^2.2.2",
"@t3-oss/env-nextjs": "^0.12.0",
"@testcontainers/mysql": "^10.18.0",
"better-sqlite3": "^11.8.1",
"dotenv": "^16.4.7",

View File

@@ -24,7 +24,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@t3-oss/env-nextjs": "^0.12.0",
"@homarr/env": "workspace:^0.1.0",
"dockerode": "^4.0.4"
},
"devDependencies": {

View File

@@ -1,7 +1,6 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
import { shouldSkipEnvValidation } from "@homarr/common/env-validation";
import { createEnv } from "@homarr/env";
export const env = createEnv({
server: {
@@ -9,10 +8,5 @@ export const env = createEnv({
DOCKER_HOSTNAMES: z.string().optional(),
DOCKER_PORTS: z.string().optional(),
},
runtimeEnv: {
DOCKER_HOSTNAMES: process.env.DOCKER_HOSTNAMES,
DOCKER_PORTS: process.env.DOCKER_PORTS,
},
skipValidation: shouldSkipEnvValidation(),
emptyStringAsUndefined: true,
experimental__runtimeEnv: process.env,
});

9
packages/env/eslint.config.js vendored Normal file
View File

@@ -0,0 +1,9 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];

1
packages/env/index.ts vendored Normal file
View File

@@ -0,0 +1 @@
export * from "./src";

36
packages/env/package.json vendored Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "@homarr/env",
"version": "0.1.0",
"private": true,
"license": "MIT",
"type": "module",
"exports": {
".": "./index.ts",
"./schemas": "./src/schemas.ts"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@t3-oss/env-nextjs": "^0.12.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.20.1",
"typescript": "^5.7.3"
}
}

9
packages/env/src/index.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
import { createEnv as createEnvT3 } from "@t3-oss/env-nextjs";
export const defaultEnvOptions = {
emptyStringAsUndefined: true,
skipValidation:
Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION) || process.env.npm_lifecycle_event === "lint",
} satisfies Partial<Parameters<typeof createEnvT3>[0]>;
export const createEnv: typeof createEnvT3 = (options) => createEnvT3({ ...defaultEnvOptions, ...options });

39
packages/env/src/schemas.ts vendored Normal file
View File

@@ -0,0 +1,39 @@
import { z } from "zod";
const trueStrings = ["1", "yes", "t", "true"];
const falseStrings = ["0", "no", "f", "false"];
export const createBooleanSchema = (defaultValue: boolean) =>
z
.string()
.default(defaultValue.toString())
.transform((value, ctx) => {
const normalized = value.trim().toLowerCase();
if (trueStrings.includes(normalized)) return true;
if (falseStrings.includes(normalized)) return false;
throw new Error(`Invalid boolean value for ${ctx.path.join(".")}`);
});
export const createDurationSchema = (defaultValue: `${number}${"s" | "m" | "h" | "d"}`) =>
z
.string()
.regex(/^\d+[smhd]?$/)
.default(defaultValue)
.transform((duration) => {
const lastChar = duration[duration.length - 1] as "s" | "m" | "h" | "d";
if (!isNaN(Number(lastChar))) {
return Number(defaultValue);
}
const multipliers = {
s: 1,
m: 60,
h: 60 * 60,
d: 60 * 60 * 24,
};
const numberDuration = Number(duration.slice(0, -1));
const multiplier = multipliers[lastChar];
return numberDuration * multiplier;
});

8
packages/env/tsconfig.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}

View File

@@ -5,11 +5,8 @@
"license": "MIT",
"type": "module",
"exports": {
".": {
"types": "./src/index.d.ts",
"default": "./src/index.mjs"
},
"./override": "./src/override.cjs"
".": "./src/index.ts",
"./env": "./src/env.ts"
},
"typesVersions": {
"*": {
@@ -26,9 +23,11 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/env": "workspace:^0.1.0",
"ioredis": "5.5.0",
"superjson": "2.2.2",
"winston": "3.17.0"
"winston": "3.17.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",

10
packages/log/src/env.ts Normal file
View File

@@ -0,0 +1,10 @@
import { z } from "zod";
import { createEnv } from "@homarr/env";
export const env = createEnv({
server: {
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
},
experimental__runtimeEnv: process.env,
});

View File

@@ -1,4 +0,0 @@
import type { Logger } from "winston";
// The following is just to make prettier happy
export const logger: Logger = undefined as unknown as Logger;

View File

@@ -1,12 +1,14 @@
import type { transport as Transport } from "winston";
import winston, { format, transports } from "winston";
import { RedisTransport } from "./redis-transport.mjs";
import { env } from "./env";
import { RedisTransport } from "./redis-transport";
const logMessageFormat = format.printf(({ level, message, timestamp }) => {
return `${timestamp} ${level}: ${message}`;
return `${timestamp as string} ${level}: ${message as string}`;
});
const logTransports = [new transports.Console()];
const logTransports: Transport[] = [new transports.Console()];
// Only add the Redis transport if we are not in CI
if (!(Boolean(process.env.CI) || Boolean(process.env.DISABLE_REDIS_LOGS))) {
@@ -16,6 +18,7 @@ if (!(Boolean(process.env.CI) || Boolean(process.env.DISABLE_REDIS_LOGS))) {
const logger = winston.createLogger({
format: format.combine(format.colorize(), format.timestamp(), logMessageFormat),
transports: logTransports,
level: env.LOG_LEVEL,
});
export { logger };

View File

@@ -1,42 +0,0 @@
void (async () => {
const { logger } = await import("./index.mjs");
const nextLogger = require("next/dist/build/output/log");
const getWinstonMethodForConsole = (consoleMethod) => {
switch (consoleMethod) {
case "error":
return (...messages) => logger.error(messages.join(" "));
case "warn":
return (...messages) => logger.warn(messages.join(" "));
case "debug":
return (...messages) => logger.debug(messages.join(" "));
case "log":
case "info":
default:
return (...messages) => logger.info(messages.join(" "));
}
};
const consoleMethods = ["log", "debug", "info", "warn", "error"];
consoleMethods.forEach((method) => {
console[method] = getWinstonMethodForConsole(method);
});
const getWinstonMethodForNext = (nextMethod) => {
switch (nextMethod) {
case "error":
return (...messages) => logger.error(messages.join(" "));
case "warn":
return (...messages) => logger.warn(messages.join(" "));
case "trace":
return (...messages) => logger.info(messages.join(" "));
default:
return (...messages) => logger.info(messages.join(" "));
}
};
Object.keys(nextLogger.prefixes).forEach((method) => {
nextLogger[method] = getWinstonMethodForNext(method);
});
})();

View File

@@ -7,15 +7,12 @@ import Transport from "winston-transport";
// of the base functionality and `.exceptions.handle()`.
//
export class RedisTransport extends Transport {
/** @type {Redis} */
redis;
private redis: Redis | null = null;
/**
* Log the info to the Redis channel
* @param {{ message: string; timestamp: string; level: string; }} info
* @param {() => void} callback
*/
log(info, callback) {
log(info: { message: string; timestamp: string; level: string }, callback: () => void) {
setImmediate(() => {
this.emit("logged", info);
});

View File

@@ -3,6 +3,6 @@
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src", "index.mjs"],
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}