feat(logging): add log level env variable (#2299)
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
9
packages/env/eslint.config.js
vendored
Normal 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
1
packages/env/index.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./src";
|
||||
36
packages/env/package.json
vendored
Normal file
36
packages/env/package.json
vendored
Normal 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
9
packages/env/src/index.ts
vendored
Normal 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
39
packages/env/src/schemas.ts
vendored
Normal 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
8
packages/env/tsconfig.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@homarr/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -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
10
packages/log/src/env.ts
Normal 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,
|
||||
});
|
||||
4
packages/log/src/index.d.ts
vendored
4
packages/log/src/index.d.ts
vendored
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
});
|
||||
})();
|
||||
@@ -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);
|
||||
});
|
||||
@@ -3,6 +3,6 @@
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "src", "index.mjs"],
|
||||
"include": ["*.ts", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user