From 67d48e11d7e1165540c66f86b299e55028d76788 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Tue, 18 Feb 2025 22:54:15 +0100 Subject: [PATCH] feat(logging): add log level env variable (#2299) --- .env.example | 2 + apps/nextjs/next.config.ts | 3 +- apps/nextjs/package.json | 1 - .../app/[locale]/_client-providers/trpc.tsx | 5 +- .../boards/(content)/_header-actions.tsx | 2 +- .../src/app/[locale]/widgets/[kind]/page.tsx | 2 +- apps/nextjs/src/env.ts | 33 ---------- packages/auth/env.ts | 36 ++--------- packages/auth/package.json | 2 +- packages/certificates/src/server.ts | 3 +- packages/common/env.ts | 9 +-- packages/common/package.json | 1 + packages/db/env.ts | 21 ++----- packages/db/package.json | 2 +- packages/docker/package.json | 2 +- packages/docker/src/env.ts | 10 +-- packages/env/eslint.config.js | 9 +++ packages/env/index.ts | 1 + packages/env/package.json | 36 +++++++++++ packages/env/src/index.ts | 9 +++ packages/env/src/schemas.ts | 39 ++++++++++++ packages/env/tsconfig.json | 8 +++ packages/log/package.json | 11 ++-- packages/log/src/env.ts | 10 +++ packages/log/src/index.d.ts | 4 -- packages/log/src/{index.mjs => index.ts} | 9 ++- packages/log/src/override.cjs | 42 ------------- ...redis-transport.mjs => redis-transport.ts} | 7 +-- packages/log/tsconfig.json | 2 +- pnpm-lock.yaml | 63 ++++++++++++++----- turbo.json | 1 + 31 files changed, 202 insertions(+), 183 deletions(-) delete mode 100644 apps/nextjs/src/env.ts create mode 100644 packages/env/eslint.config.js create mode 100644 packages/env/index.ts create mode 100644 packages/env/package.json create mode 100644 packages/env/src/index.ts create mode 100644 packages/env/src/schemas.ts create mode 100644 packages/env/tsconfig.json create mode 100644 packages/log/src/env.ts delete mode 100644 packages/log/src/index.d.ts rename packages/log/src/{index.mjs => index.ts} (62%) delete mode 100644 packages/log/src/override.cjs rename packages/log/src/{redis-transport.mjs => redis-transport.ts} (84%) diff --git a/.env.example b/.env.example index 3bc94ce26..fbc491bae 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,8 @@ AUTH_SECRET="supersecret" # or starting the project without any (which will show a randomly generated one). SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 +LOG_LEVEL='info' + # This is how you can use the sqlite driver: DB_DRIVER='better-sqlite3' DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE' diff --git a/apps/nextjs/next.config.ts b/apps/nextjs/next.config.ts index 53148f725..a8a022a60 100644 --- a/apps/nextjs/next.config.ts +++ b/apps/nextjs/next.config.ts @@ -2,14 +2,13 @@ import "@homarr/auth/env"; import "@homarr/db/env"; import "@homarr/common/env"; +import "@homarr/log/env"; import "@homarr/docker/env"; import type { NextConfig } from "next"; import MillionLint from "@million/lint"; import createNextIntlPlugin from "next-intl/plugin"; -import "./src/env.ts"; - // Package path does not work... so we need to use relative path const withNextIntl = createNextIntlPlugin("../../packages/translation/src/request.ts"); diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index a76e525da..296abc319 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -55,7 +55,6 @@ "@mantine/modals": "^7.17.0", "@mantine/tiptap": "^7.17.0", "@million/lint": "1.0.14", - "@t3-oss/env-nextjs": "^0.12.0", "@tabler/icons-react": "^3.30.0", "@tanstack/react-query": "^5.66.7", "@tanstack/react-query-devtools": "^5.66.7", diff --git a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx index 456c09a99..a46595b23 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx +++ b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx @@ -20,8 +20,7 @@ import type { SuperJSONResult } from "superjson"; import type { AppRouter } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared"; - -import { env } from "~/env"; +import { env } from "@homarr/common/env"; const getWebSocketProtocol = () => { // window is not defined on server side @@ -66,7 +65,7 @@ export function TRPCReactProvider(props: PropsWithChildren) { links: [ loggerLink({ enabled: (opts) => - process.env.NODE_ENV === "development" || (opts.direction === "down" && opts.result instanceof Error), + env.NODE_ENV === "development" || (opts.direction === "down" && opts.result instanceof Error), }), splitLink({ condition: ({ type }) => type === "subscription", diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx index e4029bd3a..6b3153b46 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx @@ -23,6 +23,7 @@ import { clientApi } from "@homarr/api/client"; import { useRequiredBoard } from "@homarr/boards/context"; import { useEditMode } from "@homarr/boards/edit-mode"; import { revalidatePathActionAsync } from "@homarr/common/client"; +import { env } from "@homarr/common/env"; import { useConfirmModal, useModalAction } from "@homarr/modals"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; @@ -33,7 +34,6 @@ import { useCategoryActions } from "~/components/board/sections/category/categor import { CategoryEditModal } from "~/components/board/sections/category/category-edit-modal"; import { useDynamicSectionActions } from "~/components/board/sections/dynamic/dynamic-actions"; import { HeaderButton } from "~/components/layout/header/button"; -import { env } from "~/env"; export const BoardContentHeaderActions = () => { const [isEditMode] = useEditMode(); diff --git a/apps/nextjs/src/app/[locale]/widgets/[kind]/page.tsx b/apps/nextjs/src/app/[locale]/widgets/[kind]/page.tsx index bcffbb4e2..fbcd741c2 100644 --- a/apps/nextjs/src/app/[locale]/widgets/[kind]/page.tsx +++ b/apps/nextjs/src/app/[locale]/widgets/[kind]/page.tsx @@ -1,11 +1,11 @@ import { notFound } from "next/navigation"; import { Center } from "@mantine/core"; +import { env } from "@homarr/common/env"; import { db } from "@homarr/db"; import type { WidgetKind } from "@homarr/definitions"; import { widgetImports } from "@homarr/widgets"; -import { env } from "~/env"; import { WidgetPreviewPageContent } from "./_content"; interface Props { diff --git a/apps/nextjs/src/env.ts b/apps/nextjs/src/env.ts deleted file mode 100644 index f3e419641..000000000 --- a/apps/nextjs/src/env.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createEnv } from "@t3-oss/env-nextjs"; -import { z } from "zod"; - -import { shouldSkipEnvValidation } from "@homarr/common/env-validation"; - -export const env = createEnv({ - shared: { - PORT: z.coerce.number().default(3000), - NODE_ENV: z.enum(["development", "production", "test"]).default("development"), - }, - /** - * Specify your server-side environment variables schema here. This way you can ensure the app isn't - * built with invalid env vars. - */ - server: {}, - /** - * Specify your client-side environment variables schema here. - * For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`. - */ - client: { - // NEXT_PUBLIC_CLIENTVAR: z.string(), - }, - /** - * Destructure all variables from `process.env` to make sure they aren't tree-shaken away. - */ - runtimeEnv: { - PORT: process.env.PORT, - NODE_ENV: process.env.NODE_ENV, - // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, - }, - skipValidation: shouldSkipEnvValidation(), - emptyStringAsUndefined: true, -}); diff --git a/packages/auth/env.ts b/packages/auth/env.ts index 5dea7de46..12de5f558 100644 --- a/packages/auth/env.ts +++ b/packages/auth/env.ts @@ -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, }); diff --git a/packages/auth/package.json b/packages/auth/package.json index ce285d358..45d039615 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -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", diff --git a/packages/certificates/src/server.ts b/packages/certificates/src/server.ts index 334c1f47f..9146896e1 100644 --- a/packages/certificates/src/server.ts +++ b/packages/certificates/src/server.ts @@ -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; }; diff --git a/packages/common/env.ts b/packages/common/env.ts index 2faeda250..9149bfc46 100644 --- a/packages/common/env.ts +++ b/packages/common/env.ts @@ -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, }); diff --git a/packages/common/package.json b/packages/common/package.json index ff831fc24..6db94b199 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -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", diff --git a/packages/db/env.ts b/packages/db/env.ts index 8e25601ce..dd8590b26 100644 --- a/packages/db/env.ts +++ b/packages/db/env.ts @@ -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, }); diff --git a/packages/db/package.json b/packages/db/package.json index 9f9beac8a..fe1106b8f 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -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", diff --git a/packages/docker/package.json b/packages/docker/package.json index 34a7d5b21..c9cd31982 100644 --- a/packages/docker/package.json +++ b/packages/docker/package.json @@ -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": { diff --git a/packages/docker/src/env.ts b/packages/docker/src/env.ts index 0f4984961..437684454 100644 --- a/packages/docker/src/env.ts +++ b/packages/docker/src/env.ts @@ -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, }); diff --git a/packages/env/eslint.config.js b/packages/env/eslint.config.js new file mode 100644 index 000000000..5b19b6f8a --- /dev/null +++ b/packages/env/eslint.config.js @@ -0,0 +1,9 @@ +import baseConfig from "@homarr/eslint-config/base"; + +/** @type {import('typescript-eslint').Config} */ +export default [ + { + ignores: [], + }, + ...baseConfig, +]; diff --git a/packages/env/index.ts b/packages/env/index.ts new file mode 100644 index 000000000..3bd16e178 --- /dev/null +++ b/packages/env/index.ts @@ -0,0 +1 @@ +export * from "./src"; diff --git a/packages/env/package.json b/packages/env/package.json new file mode 100644 index 000000000..5bd53a22b --- /dev/null +++ b/packages/env/package.json @@ -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" + } +} diff --git a/packages/env/src/index.ts b/packages/env/src/index.ts new file mode 100644 index 000000000..e77d8f61a --- /dev/null +++ b/packages/env/src/index.ts @@ -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[0]>; + +export const createEnv: typeof createEnvT3 = (options) => createEnvT3({ ...defaultEnvOptions, ...options }); diff --git a/packages/env/src/schemas.ts b/packages/env/src/schemas.ts new file mode 100644 index 000000000..da700f6ad --- /dev/null +++ b/packages/env/src/schemas.ts @@ -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; + }); diff --git a/packages/env/tsconfig.json b/packages/env/tsconfig.json new file mode 100644 index 000000000..cbe8483d9 --- /dev/null +++ b/packages/env/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@homarr/tsconfig/base.json", + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["*.ts", "src"], + "exclude": ["node_modules"] +} diff --git a/packages/log/package.json b/packages/log/package.json index 5e2dcabfb..e0b479380 100644 --- a/packages/log/package.json +++ b/packages/log/package.json @@ -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", diff --git a/packages/log/src/env.ts b/packages/log/src/env.ts new file mode 100644 index 000000000..544c29db0 --- /dev/null +++ b/packages/log/src/env.ts @@ -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, +}); diff --git a/packages/log/src/index.d.ts b/packages/log/src/index.d.ts deleted file mode 100644 index 628eec4f0..000000000 --- a/packages/log/src/index.d.ts +++ /dev/null @@ -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; diff --git a/packages/log/src/index.mjs b/packages/log/src/index.ts similarity index 62% rename from packages/log/src/index.mjs rename to packages/log/src/index.ts index 030745eb6..dcf41cb88 100644 --- a/packages/log/src/index.mjs +++ b/packages/log/src/index.ts @@ -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 }; diff --git a/packages/log/src/override.cjs b/packages/log/src/override.cjs deleted file mode 100644 index 8d4c5ca94..000000000 --- a/packages/log/src/override.cjs +++ /dev/null @@ -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); - }); -})(); diff --git a/packages/log/src/redis-transport.mjs b/packages/log/src/redis-transport.ts similarity index 84% rename from packages/log/src/redis-transport.mjs rename to packages/log/src/redis-transport.ts index b63f81afe..d56bb8302 100644 --- a/packages/log/src/redis-transport.mjs +++ b/packages/log/src/redis-transport.ts @@ -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); }); diff --git a/packages/log/tsconfig.json b/packages/log/tsconfig.json index 3fda3608b..cbe8483d9 100644 --- a/packages/log/tsconfig.json +++ b/packages/log/tsconfig.json @@ -3,6 +3,6 @@ "compilerOptions": { "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" }, - "include": ["*.ts", "src", "index.mjs"], + "include": ["*.ts", "src"], "exclude": ["node_modules"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b236c96de..c0f6d20ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -202,9 +202,6 @@ importers: '@million/lint': specifier: 1.0.14 version: 1.0.14(rollup@4.21.3)(webpack-sources@3.2.3) - '@t3-oss/env-nextjs': - specifier: ^0.12.0 - version: 0.12.0(typescript@5.7.3)(zod@3.24.2) '@tabler/icons-react': specifier: ^3.30.0 version: 3.30.0(react@19.0.0) @@ -651,15 +648,15 @@ importers: '@homarr/definitions': specifier: workspace:^0.1.0 version: link:../definitions + '@homarr/env': + specifier: workspace:^0.1.0 + version: link:../env '@homarr/log': specifier: workspace:^0.1.0 version: link:../log '@homarr/validation': specifier: workspace:^0.1.0 version: link:../validation - '@t3-oss/env-nextjs': - specifier: ^0.12.0 - version: 0.12.0(typescript@5.7.3)(zod@3.24.2) bcrypt: specifier: ^5.1.1 version: 5.1.1 @@ -802,6 +799,9 @@ importers: packages/common: dependencies: + '@homarr/env': + specifier: workspace:^0.1.0 + version: link:../env '@homarr/log': specifier: workspace:^0.1.0 version: link:../log @@ -999,6 +999,9 @@ importers: '@homarr/definitions': specifier: workspace:^0.1.0 version: link:../definitions + '@homarr/env': + specifier: workspace:^0.1.0 + version: link:../env '@homarr/log': specifier: workspace:^0.1.0 version: link:../log @@ -1011,9 +1014,6 @@ importers: '@paralleldrive/cuid2': specifier: ^2.2.2 version: 2.2.2 - '@t3-oss/env-nextjs': - specifier: ^0.12.0 - version: 0.12.0(typescript@5.7.3)(zod@3.24.2) '@testcontainers/mysql': specifier: ^10.18.0 version: 10.18.0 @@ -1091,9 +1091,9 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../common - '@t3-oss/env-nextjs': - specifier: ^0.12.0 - version: 0.12.0(typescript@5.7.3)(zod@3.24.2) + '@homarr/env': + specifier: workspace:^0.1.0 + version: link:../env dockerode: specifier: ^4.0.4 version: 4.0.4 @@ -1117,6 +1117,31 @@ importers: specifier: ^5.7.3 version: 5.7.3 + packages/env: + dependencies: + '@t3-oss/env-nextjs': + specifier: ^0.12.0 + version: 0.12.0(typescript@5.7.3)(zod@3.24.2) + zod: + specifier: ^3.24.2 + version: 3.24.2 + devDependencies: + '@homarr/eslint-config': + specifier: workspace:^0.2.0 + version: link:../../tooling/eslint + '@homarr/prettier-config': + specifier: workspace:^0.1.0 + version: link:../../tooling/prettier + '@homarr/tsconfig': + specifier: workspace:^0.1.0 + version: link:../../tooling/typescript + eslint: + specifier: ^9.20.1 + version: 9.20.1 + typescript: + specifier: ^5.7.3 + version: 5.7.3 + packages/form: dependencies: '@homarr/common': @@ -1300,6 +1325,9 @@ importers: packages/log: dependencies: + '@homarr/env': + specifier: workspace:^0.1.0 + version: link:../env ioredis: specifier: 5.5.0 version: 5.5.0 @@ -1309,6 +1337,9 @@ importers: winston: specifier: 3.17.0 version: 3.17.0 + zod: + specifier: ^3.24.2 + version: 3.24.2 devDependencies: '@homarr/eslint-config': specifier: workspace:^0.2.0 @@ -16475,7 +16506,7 @@ snapshots: dependencies: '@babel/code-frame': 7.26.2 index-to-position: 0.1.2 - type-fest: 4.34.1 + type-fest: 4.30.2 parse-ms@3.0.0: {} @@ -17039,14 +17070,14 @@ snapshots: dependencies: find-up-simple: 1.0.0 read-pkg: 9.0.1 - type-fest: 4.34.1 + type-fest: 4.30.2 read-pkg@9.0.1: dependencies: '@types/normalize-package-data': 2.4.4 normalize-package-data: 6.0.2 parse-json: 8.1.0 - type-fest: 4.34.1 + type-fest: 4.30.2 unicorn-magic: 0.1.0 readable-stream@2.3.8: @@ -18354,7 +18385,7 @@ snapshots: consola: 3.2.3 defu: 6.1.4 mime: 3.0.0 - node-fetch-native: 1.6.6 + node-fetch-native: 1.6.4 pathe: 1.1.2 unicode-emoji-modifier-base@1.0.0: {} diff --git a/turbo.json b/turbo.json index 20ccadef0..94702dfb9 100644 --- a/turbo.json +++ b/turbo.json @@ -39,6 +39,7 @@ "NODE_ENV", "PORT", "LOCAL_CERTIFICATE_PATH", + "LOG_LEVEL", "SECRET_ENCRYPTION_KEY", "SKIP_ENV_VALIDATION" ],