diff --git a/apps/nextjs/next.config.ts b/apps/nextjs/next.config.ts index 7b9bcb915..54449b397 100644 --- a/apps/nextjs/next.config.ts +++ b/apps/nextjs/next.config.ts @@ -2,7 +2,7 @@ import "@homarr/auth/env"; import "@homarr/db/env"; import "@homarr/common/env"; -import "@homarr/log/env"; +import "@homarr/core/infrastructure/logs/env"; import "@homarr/docker/env"; import type { NextConfig } from "next"; diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 7d30ddbc1..ee313efd0 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -36,7 +36,6 @@ "@homarr/icons": "workspace:^0.1.0", "@homarr/image-proxy": "workspace:^0.1.0", "@homarr/integrations": "workspace:^0.1.0", - "@homarr/log": "workspace:^", "@homarr/modals": "workspace:^0.1.0", "@homarr/modals-collection": "workspace:^0.1.0", "@homarr/notifications": "workspace:^0.1.0", diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx index 80d52a50f..64c9289e3 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx @@ -11,8 +11,9 @@ import { IntegrationProvider } from "@homarr/auth/client"; import { auth } from "@homarr/auth/next"; import { getIntegrationsWithPermissionsAsync } from "@homarr/auth/server"; import { isNullOrWhitespace } from "@homarr/common"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import type { WidgetKind } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { getI18n } from "@homarr/translation/server"; import { prefetchForKindAsync } from "@homarr/widgets/prefetch"; @@ -22,6 +23,8 @@ import type { Board, Item } from "../_types"; import { DynamicClientBoard } from "./_dynamic-client"; import { BoardContentHeaderActions } from "./_header-actions"; +const logger = createLogger({ module: "createBoardContentPage" }); + export type Params = Record; interface Props { @@ -57,7 +60,13 @@ export const createBoardContentPage = >( for (const [kind, items] of itemsMap) { await prefetchForKindAsync(kind, queryClient, items).catch((error) => { - logger.error(new Error("Failed to prefetch widget", { cause: error })); + logger.error( + new ErrorWithMetadata( + "Failed to prefetch widget", + { widgetKind: kind, itemCount: items.length }, + { cause: error }, + ), + ); }); } diff --git a/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx b/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx index da1880963..f377c932e 100644 --- a/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx +++ b/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx @@ -6,7 +6,7 @@ import { TRPCError } from "@trpc/server"; import { auth } from "@homarr/auth/next"; import { BoardProvider } from "@homarr/boards/context"; import { EditModeProvider } from "@homarr/boards/edit-mode"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { MainHeader } from "~/components/layout/header"; import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo"; @@ -18,6 +18,8 @@ import { CustomCss } from "./(content)/_custom-css"; import { BoardReadyProvider } from "./(content)/_ready-context"; import { BoardMantineProvider } from "./(content)/_theme"; +const logger = createLogger({ module: "createBoardLayout" }); + interface CreateBoardLayoutProps { headerActions: JSX.Element; getInitialBoardAsync: (params: TParams) => Promise; diff --git a/apps/nextjs/src/app/[locale]/manage/tools/logs/level-selection.tsx b/apps/nextjs/src/app/[locale]/manage/tools/logs/level-selection.tsx index fdfe23e18..71846ca02 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/logs/level-selection.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/logs/level-selection.tsx @@ -2,8 +2,8 @@ import { Select } from "@mantine/core"; -import type { LogLevel } from "@homarr/log/constants"; -import { logLevelConfiguration, logLevels } from "@homarr/log/constants"; +import type { LogLevel } from "@homarr/core/infrastructure/logs/constants"; +import { logLevelConfiguration, logLevels } from "@homarr/core/infrastructure/logs/constants"; import { useI18n } from "@homarr/translation/client"; import { useLogContext } from "./log-context"; diff --git a/apps/nextjs/src/app/[locale]/manage/tools/logs/log-context.tsx b/apps/nextjs/src/app/[locale]/manage/tools/logs/log-context.tsx index 97889af9a..9a58ffaaa 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/logs/log-context.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/logs/log-context.tsx @@ -3,8 +3,8 @@ import type { PropsWithChildren } from "react"; import { createContext, useContext, useMemo, useState } from "react"; -import type { LogLevel } from "@homarr/log/constants"; -import { logLevels } from "@homarr/log/constants"; +import type { LogLevel } from "@homarr/core/infrastructure/logs/constants"; +import { logLevels } from "@homarr/core/infrastructure/logs/constants"; const LogContext = createContext<{ level: LogLevel; diff --git a/apps/nextjs/src/app/[locale]/manage/tools/logs/page.tsx b/apps/nextjs/src/app/[locale]/manage/tools/logs/page.tsx index e13959412..d672947e4 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/logs/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/logs/page.tsx @@ -7,7 +7,7 @@ import "@xterm/xterm/css/xterm.css"; import { notFound } from "next/navigation"; import { auth } from "@homarr/auth/next"; -import { env } from "@homarr/log/env"; +import { logsEnv } from "@homarr/core/infrastructure/logs/env"; import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; import { fullHeightWithoutHeaderAndFooter } from "~/constants"; @@ -35,7 +35,7 @@ export default async function LogsManagementPage() { } return ( - + diff --git a/apps/nextjs/src/app/api/[...trpc]/route.ts b/apps/nextjs/src/app/api/[...trpc]/route.ts index ea992afda..fa5ced6fc 100644 --- a/apps/nextjs/src/app/api/[...trpc]/route.ts +++ b/apps/nextjs/src/app/api/[...trpc]/route.ts @@ -6,9 +6,12 @@ import { appRouter, createTRPCContext } from "@homarr/api"; import type { Session } from "@homarr/auth"; import { hashPasswordAsync } from "@homarr/auth"; import { createSessionAsync } from "@homarr/auth/server"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { db, eq } from "@homarr/db"; import { apiKeys } from "@homarr/db/schema"; -import { logger } from "@homarr/log"; + +const logger = createLogger({ module: "trpcOpenApiRoute" }); const handlerAsync = async (req: NextRequest) => { const apiKeyHeaderValue = req.headers.get("ApiKey"); @@ -27,7 +30,7 @@ const handlerAsync = async (req: NextRequest) => { router: appRouter, createContext: () => createTRPCContext({ session, headers: req.headers }), onError({ error, path, type }) { - logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause })); + logger.error(new ErrorWithMetadata("tRPC Error occured", { path, type }, { cause: error })); }, }); }; @@ -48,9 +51,10 @@ const getSessionOrDefaultFromHeadersAsync = async ( const [apiKeyId, apiKey] = apiKeyHeaderValue.split("."); if (!apiKeyId || !apiKey) { - logger.warn( - `An attempt to authenticate over API has failed due to invalid API key format ip='${ipAdress}' userAgent='${userAgent}'`, - ); + logger.warn("An attempt to authenticate over API has failed due to invalid API key format", { + ipAdress, + userAgent, + }); return null; } @@ -74,18 +78,21 @@ const getSessionOrDefaultFromHeadersAsync = async ( }); if (!apiKeyFromDb) { - logger.warn(`An attempt to authenticate over API has failed ip='${ipAdress}' userAgent='${userAgent}'`); + logger.warn("An attempt to authenticate over API has failed", { ipAdress, userAgent }); return null; } const hashedApiKey = await hashPasswordAsync(apiKey, apiKeyFromDb.salt); if (apiKeyFromDb.apiKey !== hashedApiKey) { - logger.warn(`An attempt to authenticate over API has failed ip='${ipAdress}' userAgent='${userAgent}'`); + logger.warn("An attempt to authenticate over API has failed", { ipAdress, userAgent }); return null; } - logger.info(`Read session from API request and found user ${apiKeyFromDb.user.name} (${apiKeyFromDb.user.id})`); + logger.info("Read session from API request and found user", { + name: apiKeyFromDb.user.name, + id: apiKeyFromDb.user.id, + }); return await createSessionAsync(db, apiKeyFromDb.user); }; diff --git a/apps/nextjs/src/app/api/auth/[...nextauth]/route.ts b/apps/nextjs/src/app/api/auth/[...nextauth]/route.ts index e04a79859..37a0b7feb 100644 --- a/apps/nextjs/src/app/api/auth/[...nextauth]/route.ts +++ b/apps/nextjs/src/app/api/auth/[...nextauth]/route.ts @@ -1,8 +1,10 @@ import { NextRequest } from "next/server"; import { createHandlersAsync } from "@homarr/auth"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { SupportedAuthProvider } from "@homarr/definitions"; -import { logger } from "@homarr/log"; + +const logger = createLogger({ module: "nextAuthRoute" }); export const GET = async (req: NextRequest) => { const { handlers } = await createHandlersAsync(extractProvider(req), isSecureCookieEnabled(req)); diff --git a/apps/nextjs/src/app/api/health/live/route.ts b/apps/nextjs/src/app/api/health/live/route.ts index 57cc9b207..2576b3f8e 100644 --- a/apps/nextjs/src/app/api/health/live/route.ts +++ b/apps/nextjs/src/app/api/health/live/route.ts @@ -1,13 +1,16 @@ import { performance } from "perf_hooks"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { db } from "@homarr/db"; -import { logger } from "@homarr/log"; import { handshakeAsync } from "@homarr/redis"; +const logger = createLogger({ module: "healthLiveRoute" }); + export async function GET() { const timeBeforeHealthCheck = performance.now(); const response = await executeAndAggregateAllHealthChecksAsync(); - logger.info(`Completed healthcheck after ${performance.now() - timeBeforeHealthCheck}ms`); + logger.info("Completed healthcheck", { elapsed: `${performance.now() - timeBeforeHealthCheck}ms` }); if (response.status === "healthy") { return new Response(JSON.stringify(response), { @@ -73,7 +76,7 @@ const executeHealthCheckSafelyAsync = async ( }; } catch (error) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - logger.error(`Healthcheck '${name}' has failed: ${error}`); + logger.error(new ErrorWithMetadata("Healthcheck failed", { name }, { cause: error })); return { status: "unhealthy", values: { diff --git a/apps/nextjs/src/app/api/trpc/[trpc]/route.ts b/apps/nextjs/src/app/api/trpc/[trpc]/route.ts index f35afb7a3..65ebb353d 100644 --- a/apps/nextjs/src/app/api/trpc/[trpc]/route.ts +++ b/apps/nextjs/src/app/api/trpc/[trpc]/route.ts @@ -3,7 +3,10 @@ import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; import { appRouter, createTRPCContext } from "@homarr/api"; import { trpcPath } from "@homarr/api/shared"; import { auth } from "@homarr/auth/next"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; + +const logger = createLogger({ module: "trpcRoute" }); /** * Configure basic CORS headers @@ -31,7 +34,7 @@ const handler = auth(async (req) => { req, createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }), onError({ error, path, type }) { - logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause })); + logger.error(new ErrorWithMetadata("tRPC Error occured", { path, type }, { cause: error })); }, }); diff --git a/apps/nextjs/src/errors/trpc-catch-error.ts b/apps/nextjs/src/errors/trpc-catch-error.ts index 1fb766745..3e38e9cfb 100644 --- a/apps/nextjs/src/errors/trpc-catch-error.ts +++ b/apps/nextjs/src/errors/trpc-catch-error.ts @@ -3,7 +3,9 @@ import "server-only"; import { notFound, redirect } from "next/navigation"; import { TRPCError } from "@trpc/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; + +const logger = createLogger({ module: "trpcCatchError" }); export const catchTrpcNotFound = (err: unknown) => { if (err instanceof TRPCError && err.code === "NOT_FOUND") { diff --git a/apps/tasks/package.json b/apps/tasks/package.json index ecd41e5f1..3fcad6951 100644 --- a/apps/tasks/package.json +++ b/apps/tasks/package.json @@ -23,6 +23,7 @@ "@homarr/analytics": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", + "@homarr/core": "workspace:^", "@homarr/cron-job-api": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0", "@homarr/cron-jobs-core": "workspace:^0.1.0", @@ -30,7 +31,6 @@ "@homarr/definitions": "workspace:^0.1.0", "@homarr/icons": "workspace:^0.1.0", "@homarr/integrations": "workspace:^0.1.0", - "@homarr/log": "workspace:^", "@homarr/ping": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", "@homarr/request-handler": "workspace:^0.1.0", diff --git a/apps/tasks/src/job-manager.ts b/apps/tasks/src/job-manager.ts index e19f2c315..9e23c7d36 100644 --- a/apps/tasks/src/job-manager.ts +++ b/apps/tasks/src/job-manager.ts @@ -1,11 +1,13 @@ import { schedule, validate as validateCron } from "node-cron"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IJobManager } from "@homarr/cron-job-api"; import type { jobGroup as cronJobGroup, JobGroupKeys } from "@homarr/cron-jobs"; import type { Database, InferInsertModel } from "@homarr/db"; import { eq } from "@homarr/db"; import { cronJobConfigurations } from "@homarr/db/schema"; -import { logger } from "@homarr/log"; + +const logger = createLogger({ module: "jobManager" }); export class JobManager implements IJobManager { constructor( @@ -23,7 +25,7 @@ export class JobManager implements IJobManager { await this.jobGroup.stopAsync(name); } public async updateIntervalAsync(name: JobGroupKeys, cron: string): Promise { - logger.info(`Updating cron job interval name="${name}" expression="${cron}"`); + logger.info("Updating cron job interval", { name, expression: cron }); const job = this.jobGroup.getJobRegistry().get(name); if (!job) throw new Error(`Job ${name} not found`); if (!validateCron(cron)) { @@ -38,22 +40,22 @@ export class JobManager implements IJobManager { name, }), ); - logger.info(`Cron job interval updated name="${name}" expression="${cron}"`); + logger.info("Cron job interval updated", { name, expression: cron }); } public async disableAsync(name: JobGroupKeys): Promise { - logger.info(`Disabling cron job name="${name}"`); + logger.info("Disabling cron job", { name }); const job = this.jobGroup.getJobRegistry().get(name); if (!job) throw new Error(`Job ${name} not found`); await this.updateConfigurationAsync(name, { isEnabled: false }); await this.jobGroup.stopAsync(name); - logger.info(`Cron job disabled name="${name}"`); + logger.info("Cron job disabled", { name }); } public async enableAsync(name: JobGroupKeys): Promise { - logger.info(`Enabling cron job name="${name}"`); + logger.info("Enabling cron job", { name }); await this.updateConfigurationAsync(name, { isEnabled: true }); await this.jobGroup.startAsync(name); - logger.info(`Cron job enabled name="${name}"`); + logger.info("Cron job enabled", { name }); } private async updateConfigurationAsync( @@ -64,9 +66,11 @@ export class JobManager implements IJobManager { where: (table, { eq }) => eq(table.name, name), }); - logger.debug( - `Updating cron job configuration name="${name}" configuration="${JSON.stringify(configuration)}" exists="${Boolean(existingConfig)}"`, - ); + logger.debug("Updating cron job configuration", { + name, + configuration: JSON.stringify(configuration), + exists: Boolean(existingConfig), + }); if (existingConfig) { await this.db @@ -74,7 +78,10 @@ export class JobManager implements IJobManager { // prevent updating the name, as it is the primary key .set({ ...configuration, name: undefined }) .where(eq(cronJobConfigurations.name, name)); - logger.debug(`Cron job configuration updated name="${name}" configuration="${JSON.stringify(configuration)}"`); + logger.debug("Cron job configuration updated", { + name, + configuration: JSON.stringify(configuration), + }); return; } @@ -86,7 +93,10 @@ export class JobManager implements IJobManager { cronExpression: configuration.cronExpression ?? job.cronExpression, isEnabled: configuration.isEnabled ?? true, }); - logger.debug(`Cron job configuration updated name="${name}" configuration="${JSON.stringify(configuration)}"`); + logger.debug("Cron job configuration updated", { + name, + configuration: JSON.stringify(configuration), + }); } public async getAllAsync(): Promise< diff --git a/apps/tasks/src/main.ts b/apps/tasks/src/main.ts index ad6181880..4c60f1646 100644 --- a/apps/tasks/src/main.ts +++ b/apps/tasks/src/main.ts @@ -5,16 +5,19 @@ import type { FastifyTRPCPluginOptions } from "@trpc/server/adapters/fastify"; import { fastifyTRPCPlugin } from "@trpc/server/adapters/fastify"; import fastify from "fastify"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import type { JobRouter } from "@homarr/cron-job-api"; import { jobRouter } from "@homarr/cron-job-api"; import { CRON_JOB_API_KEY_HEADER, CRON_JOB_API_PATH, CRON_JOB_API_PORT } from "@homarr/cron-job-api/constants"; import { jobGroup } from "@homarr/cron-jobs"; import { db } from "@homarr/db"; -import { logger } from "@homarr/log"; import { JobManager } from "./job-manager"; import { onStartAsync } from "./on-start"; +const logger = createLogger({ module: "tasksMain" }); + const server = fastify({ maxParamLength: 5000, }); @@ -27,7 +30,7 @@ server.register(fastifyTRPCPlugin, { apiKey: req.headers[CRON_JOB_API_KEY_HEADER] as string | undefined, }), onError({ path, error }) { - logger.error(new Error(`Error in tasks tRPC handler path="${path}"`, { cause: error })); + logger.error(new ErrorWithMetadata("Error in tasks tRPC handler", { path }, { cause: error })); }, } satisfies FastifyTRPCPluginOptions["trpcOptions"], }); @@ -39,9 +42,11 @@ void (async () => { try { await server.listen({ port: CRON_JOB_API_PORT }); - logger.info(`Tasks web server started successfully port="${CRON_JOB_API_PORT}"`); + logger.info("Tasks web server started successfully", { port: CRON_JOB_API_PORT }); } catch (err) { - logger.error(new Error(`Failed to start tasks web server port="${CRON_JOB_API_PORT}"`, { cause: err })); + logger.error( + new ErrorWithMetadata("Failed to start tasks web server", { port: CRON_JOB_API_PORT }, { cause: err }), + ); process.exit(1); } })(); diff --git a/apps/tasks/src/on-start/invalidate-update-checker-cache.ts b/apps/tasks/src/on-start/invalidate-update-checker-cache.ts index 7bbda3dec..470989dcf 100644 --- a/apps/tasks/src/on-start/invalidate-update-checker-cache.ts +++ b/apps/tasks/src/on-start/invalidate-update-checker-cache.ts @@ -1,7 +1,7 @@ -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker"; -const localLogger = logger.child({ module: "invalidateUpdateCheckerCache" }); +const logger = createLogger({ module: "invalidateUpdateCheckerCache" }); /** * Invalidates the update checker cache on startup to ensure fresh data. @@ -11,8 +11,8 @@ export async function invalidateUpdateCheckerCacheAsync() { try { const handler = updateCheckerRequestHandler.handler({}); await handler.invalidateAsync(); - localLogger.debug("Update checker cache invalidated"); + logger.debug("Update checker cache invalidated"); } catch (error) { - localLogger.error(new Error("Failed to invalidate update checker cache", { cause: error })); + logger.error(new Error("Failed to invalidate update checker cache", { cause: error })); } } diff --git a/apps/tasks/src/on-start/session-cleanup.ts b/apps/tasks/src/on-start/session-cleanup.ts index 4dfe46341..3ef6d9cdc 100644 --- a/apps/tasks/src/on-start/session-cleanup.ts +++ b/apps/tasks/src/on-start/session-cleanup.ts @@ -1,10 +1,10 @@ import { env } from "@homarr/auth/env"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { db, eq, inArray } from "@homarr/db"; import { sessions, users } from "@homarr/db/schema"; import { supportedAuthProviders } from "@homarr/definitions"; -import { logger } from "@homarr/log"; -const localLogger = logger.child({ module: "sessionCleanup" }); +const logger = createLogger({ module: "sessionCleanup" }); /** * Deletes sessions for users that have inactive auth providers. @@ -29,11 +29,13 @@ export async function cleanupSessionsAsync() { await db.delete(sessions).where(inArray(sessions.userId, userIds)); if (sessionsWithInactiveProviders.length > 0) { - localLogger.info(`Deleted sessions for inactive providers count=${userIds.length}`); + logger.info("Deleted sessions for inactive providers", { + count: userIds.length, + }); } else { - localLogger.debug("No sessions to delete"); + logger.debug("No sessions to delete"); } } catch (error) { - localLogger.error(new Error("Failed to clean up sessions", { cause: error })); + logger.error(new Error("Failed to clean up sessions", { cause: error })); } } diff --git a/apps/websocket/package.json b/apps/websocket/package.json index 3f77645a8..e92653c96 100644 --- a/apps/websocket/package.json +++ b/apps/websocket/package.json @@ -20,9 +20,9 @@ "@homarr/api": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", + "@homarr/core": "workspace:^", "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", - "@homarr/log": "workspace:^", "@homarr/redis": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "dotenv": "^17.2.3", diff --git a/apps/websocket/src/main.ts b/apps/websocket/src/main.ts index f819c3dff..45dfe50a2 100644 --- a/apps/websocket/src/main.ts +++ b/apps/websocket/src/main.ts @@ -4,8 +4,10 @@ import { WebSocketServer } from "ws"; import { appRouter, createTRPCContext } from "@homarr/api/websocket"; import { getSessionFromToken, sessionTokenCookieName } from "@homarr/auth"; import { parseCookies } from "@homarr/common"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { db } from "@homarr/db"; -import { logger } from "@homarr/log"; + +const logger = createLogger({ module: "websocketMain" }); const wss = new WebSocketServer({ port: 3001, diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 913f55d22..b88d65990 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -22,8 +22,8 @@ }, "prettier": "@homarr/prettier-config", "dependencies": { + "@homarr/core": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", - "@homarr/log": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", "@umami/node": "^0.4.0", "superjson": "2.2.6" diff --git a/packages/analytics/src/send-server-analytics.ts b/packages/analytics/src/send-server-analytics.ts index cdf94e134..27f1ae3d8 100644 --- a/packages/analytics/src/send-server-analytics.ts +++ b/packages/analytics/src/send-server-analytics.ts @@ -1,15 +1,17 @@ import type { UmamiEventData } from "@umami/node"; import { Umami } from "@umami/node"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { count, db } from "@homarr/db"; import { getServerSettingByKeyAsync } from "@homarr/db/queries"; import { integrations, items, users } from "@homarr/db/schema"; -import { logger } from "@homarr/log"; import type { defaultServerSettings } from "@homarr/server-settings"; import { Stopwatch } from "../../common/src"; import { UMAMI_HOST_URL, UMAMI_WEBSITE_ID } from "./constants"; +const logger = createLogger({ module: "analytics" }); + export const sendServerAnalyticsAsync = async () => { const stopWatch = new Stopwatch(); const analyticsSettings = await getServerSettingByKeyAsync(db, "analytics"); diff --git a/packages/api/package.json b/packages/api/package.json index e0fab68a6..2f044a0e6 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -33,7 +33,6 @@ "@homarr/docker": "workspace:^0.1.0", "@homarr/icons": "workspace:^0.1.0", "@homarr/integrations": "workspace:^0.1.0", - "@homarr/log": "workspace:^", "@homarr/old-import": "workspace:^0.1.0", "@homarr/old-schema": "workspace:^0.1.0", "@homarr/ping": "workspace:^0.1.0", diff --git a/packages/api/src/router/certificates/certificate-router.ts b/packages/api/src/router/certificates/certificate-router.ts index 4c7cfc862..ba758a6bb 100644 --- a/packages/api/src/router/certificates/certificate-router.ts +++ b/packages/api/src/router/certificates/certificate-router.ts @@ -4,13 +4,15 @@ import { zfd } from "zod-form-data"; import { z } from "zod/v4"; import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { and, eq } from "@homarr/db"; import { trustedCertificateHostnames } from "@homarr/db/schema"; -import { logger } from "@homarr/log"; import { certificateValidFileNameSchema, checkCertificateFile } from "@homarr/validation/certificates"; import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc"; +const logger = createLogger({ module: "certificateRouter" }); + export const certificateRouter = createTRPCRouter({ addCertificate: permissionRequiredProcedure .requiresPermission("admin") diff --git a/packages/api/src/router/cron-jobs.ts b/packages/api/src/router/cron-jobs.ts index 4564ec27a..5428ac374 100644 --- a/packages/api/src/router/cron-jobs.ts +++ b/packages/api/src/router/cron-jobs.ts @@ -1,14 +1,16 @@ import { observable } from "@trpc/server/observable"; import z from "zod/v4"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { cronExpressionSchema, jobGroupKeys, jobNameSchema } from "@homarr/cron-job-api"; import { cronJobApi } from "@homarr/cron-job-api/client"; import type { TaskStatus } from "@homarr/cron-job-status"; import { createCronJobStatusChannel } from "@homarr/cron-job-status"; -import { logger } from "@homarr/log"; import { createTRPCRouter, permissionRequiredProcedure } from "../trpc"; +const logger = createLogger({ module: "cronJobsRouter" }); + export const cronJobsRouter = createTRPCRouter({ triggerJob: permissionRequiredProcedure .requiresPermission("admin") diff --git a/packages/api/src/router/integration/integration-router.ts b/packages/api/src/router/integration/integration-router.ts index 23eca9fb5..4a12e062e 100644 --- a/packages/api/src/router/integration/integration-router.ts +++ b/packages/api/src/router/integration/integration-router.ts @@ -3,6 +3,7 @@ import { z } from "zod/v4"; import { createId, objectEntries } from "@homarr/common"; import { decryptSecret, encryptSecret } from "@homarr/common/server"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { Database } from "@homarr/db"; import { and, asc, eq, handleTransactionsAsync, inArray, like, or } from "@homarr/db"; import { @@ -26,7 +27,6 @@ import { integrationSecretKindObject, } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; -import { logger } from "@homarr/log"; import { byIdSchema } from "@homarr/validation/common"; import { integrationCreateSchema, @@ -40,6 +40,8 @@ import { throwIfActionForbiddenAsync } from "./integration-access"; import { MissingSecretError, testConnectionAsync } from "./integration-test-connection"; import { mapTestConnectionError } from "./map-test-connection-error"; +const logger = createLogger({ module: "integrationRouter" }); + export const integrationRouter = createTRPCRouter({ all: publicProcedure.query(async ({ ctx }) => { const groupsOfCurrentUser = await ctx.db.query.groupMembers.findMany({ diff --git a/packages/api/src/router/integration/integration-test-connection.ts b/packages/api/src/router/integration/integration-test-connection.ts index b2084b6a3..a1c735f25 100644 --- a/packages/api/src/router/integration/integration-test-connection.ts +++ b/packages/api/src/router/integration/integration-test-connection.ts @@ -1,9 +1,12 @@ import { decryptSecret } from "@homarr/common/server"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import type { Integration } from "@homarr/db/schema"; import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions"; import { getAllSecretKindOptions } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; -import { logger } from "@homarr/log"; + +const logger = createLogger({ module: "integrationTestConnection" }); type FormIntegration = Omit & { secrets: { @@ -35,8 +38,13 @@ export const testConnectionAsync = async ( }; } catch (error) { logger.warn( - new Error( - `Failed to decrypt secret from database integration="${integration.name}" secretKind="${secret.kind}"`, + new ErrorWithMetadata( + "Failed to decrypt secret from database", + { + integrationName: integration.name, + integrationKind: integration.kind, + secretKind: secret.kind, + }, { cause: error }, ), ); diff --git a/packages/api/src/router/kubernetes/router/cluster.ts b/packages/api/src/router/kubernetes/router/cluster.ts index e262ab8fa..adcb72389 100644 --- a/packages/api/src/router/kubernetes/router/cluster.ts +++ b/packages/api/src/router/kubernetes/router/cluster.ts @@ -2,7 +2,6 @@ import type { V1NodeList, VersionInfo } from "@kubernetes/client-node"; import { TRPCError } from "@trpc/server"; import type { ClusterResourceCount, KubernetesCluster } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { kubernetesMiddleware } from "../../../middlewares/kubernetes"; import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc"; @@ -129,7 +128,6 @@ export const clusterRouter = createTRPCRouter({ ], }; } catch (error) { - logger.error("Unable to retrieve cluster", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "An error occurred while fetching Kubernetes cluster", @@ -165,7 +163,6 @@ export const clusterRouter = createTRPCRouter({ { label: "volumes", count: volumes.items.length }, ]; } catch (error) { - logger.error("Unable to retrieve cluster resource counts", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "An error occurred while fetching Kubernetes resources count", diff --git a/packages/api/src/router/kubernetes/router/configMaps.ts b/packages/api/src/router/kubernetes/router/configMaps.ts index c97f6e44f..ac7edeeae 100644 --- a/packages/api/src/router/kubernetes/router/configMaps.ts +++ b/packages/api/src/router/kubernetes/router/configMaps.ts @@ -1,7 +1,6 @@ import { TRPCError } from "@trpc/server"; import type { KubernetesBaseResource } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { kubernetesMiddleware } from "../../../middlewares/kubernetes"; import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc"; @@ -25,7 +24,6 @@ export const configMapsRouter = createTRPCRouter({ }; }); } catch (error) { - logger.error("Unable to retrieve configMaps", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "An error occurred while fetching Kubernetes ConfigMaps", diff --git a/packages/api/src/router/kubernetes/router/ingresses.ts b/packages/api/src/router/kubernetes/router/ingresses.ts index 17d6c97d3..a0ec41ce5 100644 --- a/packages/api/src/router/kubernetes/router/ingresses.ts +++ b/packages/api/src/router/kubernetes/router/ingresses.ts @@ -2,7 +2,6 @@ import type { V1HTTPIngressPath, V1Ingress, V1IngressRule } from "@kubernetes/cl import { TRPCError } from "@trpc/server"; import type { KubernetesIngress, KubernetesIngressPath, KubernetesIngressRuleAndPath } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { kubernetesMiddleware } from "../../../middlewares/kubernetes"; import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc"; @@ -43,7 +42,6 @@ export const ingressesRouter = createTRPCRouter({ return ingresses.items.map(mapIngress); } catch (error) { - logger.error("Unable to retrieve ingresses", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "An error occurred while fetching Kubernetes ingresses", diff --git a/packages/api/src/router/kubernetes/router/namespaces.ts b/packages/api/src/router/kubernetes/router/namespaces.ts index 889075d4a..f515af890 100644 --- a/packages/api/src/router/kubernetes/router/namespaces.ts +++ b/packages/api/src/router/kubernetes/router/namespaces.ts @@ -1,7 +1,6 @@ import { TRPCError } from "@trpc/server"; import type { KubernetesNamespace, KubernetesNamespaceState } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { kubernetesMiddleware } from "../../../middlewares/kubernetes"; import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc"; @@ -25,7 +24,6 @@ export const namespacesRouter = createTRPCRouter({ } satisfies KubernetesNamespace; }); } catch (error) { - logger.error("Unable to retrieve namespaces", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "An error occurred while fetching Kubernetes namespaces", diff --git a/packages/api/src/router/kubernetes/router/nodes.ts b/packages/api/src/router/kubernetes/router/nodes.ts index a4fc1959c..c7afe2a8b 100644 --- a/packages/api/src/router/kubernetes/router/nodes.ts +++ b/packages/api/src/router/kubernetes/router/nodes.ts @@ -1,7 +1,6 @@ import { TRPCError } from "@trpc/server"; import type { KubernetesNode, KubernetesNodeState } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { kubernetesMiddleware } from "../../../middlewares/kubernetes"; import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc"; @@ -57,7 +56,6 @@ export const nodesRouter = createTRPCRouter({ }; }); } catch (error) { - logger.error("Unable to retrieve nodes", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "An error occurred while fetching Kubernetes nodes", diff --git a/packages/api/src/router/kubernetes/router/pods.ts b/packages/api/src/router/kubernetes/router/pods.ts index fbf5fb183..707e6ff58 100644 --- a/packages/api/src/router/kubernetes/router/pods.ts +++ b/packages/api/src/router/kubernetes/router/pods.ts @@ -2,13 +2,15 @@ import type { KubeConfig, V1OwnerReference } from "@kubernetes/client-node"; import { AppsV1Api } from "@kubernetes/client-node"; import { TRPCError } from "@trpc/server"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { KubernetesPod } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { kubernetesMiddleware } from "../../../middlewares/kubernetes"; import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc"; import { KubernetesClient } from "../kubernetes-client"; +const logger = createLogger({ module: "podsRouter" }); + export const podsRouter = createTRPCRouter({ getPods: permissionRequiredProcedure .requiresPermission("admin") @@ -55,7 +57,6 @@ export const podsRouter = createTRPCRouter({ return pods; } catch (error) { - logger.error("Unable to retrieve pods", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "An error occurred while fetching Kubernetes pods", diff --git a/packages/api/src/router/kubernetes/router/secrets.ts b/packages/api/src/router/kubernetes/router/secrets.ts index 2fb80b272..2bf9b5665 100644 --- a/packages/api/src/router/kubernetes/router/secrets.ts +++ b/packages/api/src/router/kubernetes/router/secrets.ts @@ -1,7 +1,6 @@ import { TRPCError } from "@trpc/server"; import type { KubernetesSecret } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { kubernetesMiddleware } from "../../../middlewares/kubernetes"; import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc"; @@ -25,7 +24,6 @@ export const secretsRouter = createTRPCRouter({ }; }); } catch (error) { - logger.error("Unable to retrieve secrets", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "An error occurred while fetching Kubernetes secrets", diff --git a/packages/api/src/router/kubernetes/router/services.ts b/packages/api/src/router/kubernetes/router/services.ts index 94b91598c..2d42cecfb 100644 --- a/packages/api/src/router/kubernetes/router/services.ts +++ b/packages/api/src/router/kubernetes/router/services.ts @@ -1,7 +1,6 @@ import { TRPCError } from "@trpc/server"; import type { KubernetesService } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { kubernetesMiddleware } from "../../../middlewares/kubernetes"; import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc"; @@ -29,7 +28,6 @@ export const servicesRouter = createTRPCRouter({ }; }); } catch (error) { - logger.error("Unable to retrieve services", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "An error occurred while fetching Kubernetes services", diff --git a/packages/api/src/router/kubernetes/router/volumes.ts b/packages/api/src/router/kubernetes/router/volumes.ts index f1275a704..d9c824a68 100644 --- a/packages/api/src/router/kubernetes/router/volumes.ts +++ b/packages/api/src/router/kubernetes/router/volumes.ts @@ -1,7 +1,6 @@ import { TRPCError } from "@trpc/server"; import type { KubernetesVolume } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { kubernetesMiddleware } from "../../../middlewares/kubernetes"; import { createTRPCRouter, permissionRequiredProcedure } from "../../../trpc"; @@ -31,7 +30,6 @@ export const volumesRouter = createTRPCRouter({ }; }); } catch (error) { - logger.error("Unable to retrieve volumes", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "An error occurred while fetching Kubernetes Volumes", diff --git a/packages/api/src/router/log.ts b/packages/api/src/router/log.ts index 315f51de4..10d44d0ab 100644 --- a/packages/api/src/router/log.ts +++ b/packages/api/src/router/log.ts @@ -1,14 +1,16 @@ import { observable } from "@trpc/server/observable"; import z from "zod/v4"; -import { logger } from "@homarr/log"; -import { logLevels } from "@homarr/log/constants"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { logLevels } from "@homarr/core/infrastructure/logs/constants"; import type { LoggerMessage } from "@homarr/redis"; import { loggingChannel } from "@homarr/redis"; import { zodEnumFromArray } from "@homarr/validation/enums"; import { createTRPCRouter, permissionRequiredProcedure } from "../trpc"; +const logger = createLogger({ module: "logRouter" }); + export const logRouter = createTRPCRouter({ subscribe: permissionRequiredProcedure .requiresPermission("other-view-logs") diff --git a/packages/api/src/router/search-engine/search-engine-router.ts b/packages/api/src/router/search-engine/search-engine-router.ts index 6e983d38e..ddfc892a9 100644 --- a/packages/api/src/router/search-engine/search-engine-router.ts +++ b/packages/api/src/router/search-engine/search-engine-router.ts @@ -2,11 +2,11 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod/v4"; import { createId } from "@homarr/common"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { asc, eq, like } from "@homarr/db"; import { getServerSettingByKeyAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries"; import { searchEngines, users } from "@homarr/db/schema"; import { createIntegrationAsync } from "@homarr/integrations"; -import { logger } from "@homarr/log"; import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common"; import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine"; import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request"; @@ -14,6 +14,8 @@ import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/va import { createOneIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc"; +const logger = createLogger({ module: "searchEngineRouter" }); + export const searchEngineRouter = createTRPCRouter({ getPaginated: protectedProcedure.input(paginatedSchema).query(async ({ input, ctx }) => { const whereQuery = input.search ? like(searchEngines.name, `%${input.search.trim()}%`) : undefined; diff --git a/packages/api/src/router/update-checker.ts b/packages/api/src/router/update-checker.ts index 6920358ce..8957be5f2 100644 --- a/packages/api/src/router/update-checker.ts +++ b/packages/api/src/router/update-checker.ts @@ -1,8 +1,10 @@ -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker"; import { createTRPCRouter, permissionRequiredProcedure } from "../trpc"; +const logger = createLogger({ module: "updateCheckerRouter" }); + export const updateCheckerRouter = createTRPCRouter({ getAvailableUpdates: permissionRequiredProcedure.requiresPermission("admin").query(async () => { try { diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts index f59d6740e..875ffe80b 100644 --- a/packages/api/src/router/user.ts +++ b/packages/api/src/router/user.ts @@ -3,6 +3,7 @@ import { z } from "zod/v4"; import { createSaltAsync, hashPasswordAsync } from "@homarr/auth"; import { createId } from "@homarr/common"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { Database } from "@homarr/db"; import { and, eq, like } from "@homarr/db"; import { getMaxGroupPositionAsync } from "@homarr/db/queries"; @@ -10,7 +11,6 @@ import { boards, groupMembers, groupPermissions, groups, invites, users } from " import { selectUserSchema } from "@homarr/db/validationSchemas"; import { credentialsAdminGroup } from "@homarr/definitions"; import type { SupportedAuthProvider } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { byIdSchema } from "@homarr/validation/common"; import type { userBaseCreateSchema } from "@homarr/validation/user"; import { @@ -39,6 +39,8 @@ import { throwIfCredentialsDisabled } from "./invite/checks"; import { nextOnboardingStepAsync } from "./onboard/onboard-queries"; import { changeSearchPreferencesAsync, changeSearchPreferencesInputSchema } from "./user/change-search-preferences"; +const logger = createLogger({ module: "userRouter" }); + export const userRouter = createTRPCRouter({ initUser: onboardingProcedure .requiresStep("user") @@ -364,9 +366,11 @@ export const userRouter = createTRPCRouter({ // Admins can change the password of other users without providing the previous password const isPreviousPasswordRequired = ctx.session.user.id === input.userId; - logger.info( - `User ${user.id} is changing password for user ${input.userId}, previous password is required: ${isPreviousPasswordRequired}`, - ); + logger.info("Changing user password", { + actorId: ctx.session.user.id, + targetUserId: input.userId, + previousPasswordRequired: isPreviousPasswordRequired, + }); if (isPreviousPasswordRequired) { const previousPasswordHash = await hashPasswordAsync(input.previousPassword, dbUser.salt ?? ""); diff --git a/packages/api/src/router/widgets/indexer-manager.ts b/packages/api/src/router/widgets/indexer-manager.ts index b5b9ea6be..cb8b88262 100644 --- a/packages/api/src/router/widgets/indexer-manager.ts +++ b/packages/api/src/router/widgets/indexer-manager.ts @@ -4,7 +4,6 @@ import { observable } from "@trpc/server/observable"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { Indexer } from "@homarr/integrations/types"; -import { logger } from "@homarr/log"; import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-manager"; import type { IntegrationAction } from "../../middlewares/integration"; @@ -61,10 +60,10 @@ export const indexerManagerRouter = createTRPCRouter({ ctx.integrations.map(async (integration) => { const client = await createIntegrationAsync(integration); await client.testAllAsync().catch((err) => { - logger.error("indexer-manager router - ", err); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Failed to test all indexers for ${integration.name} (${integration.id})`, + cause: err, }); }); }), diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index fd5f47442..bedd60f3d 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -14,12 +14,14 @@ import { ZodError } from "zod/v4"; import type { Session } from "@homarr/auth"; import { FlattenError } from "@homarr/common"; import { userAgent } from "@homarr/common/server"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { db } from "@homarr/db"; import type { GroupPermissionKey, OnboardingStep } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { getOnboardingOrFallbackAsync } from "./router/onboard/onboard-queries"; +const logger = createLogger({ module: "trpc" }); + /** * 1. CONTEXT * @@ -36,7 +38,7 @@ export const createTRPCContext = (opts: { headers: Headers; session: Session | n const session = opts.session; const source = opts.headers.get("x-trpc-source") ?? "unknown"; - logger.info(`tRPC request from ${source} by user '${session?.user.name} (${session?.user.id})'`, session?.user); + logger.info("Received tRPC request", { source, userId: session?.user.id, userName: session?.user.name }); return { session, diff --git a/packages/auth/configuration.ts b/packages/auth/configuration.ts index 177e7e8fb..b0181e326 100644 --- a/packages/auth/configuration.ts +++ b/packages/auth/configuration.ts @@ -3,9 +3,9 @@ import { cookies } from "next/headers"; import NextAuth from "next-auth"; import Credentials from "next-auth/providers/credentials"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { db } from "@homarr/db"; import type { SupportedAuthProvider } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { createAdapter } from "./adapter"; import { createSessionCallback } from "./callbacks"; @@ -18,6 +18,8 @@ import { OidcProvider } from "./providers/oidc/oidc-provider"; import { createRedirectUri } from "./redirect"; import { expireDateAfter, generateSessionToken, sessionTokenCookieName } from "./session"; +const logger = createLogger({ module: "authConfiguration" }); + // See why it's unknown in the [...nextauth]/route.ts file export const createConfiguration = ( provider: SupportedAuthProvider | "unknown", diff --git a/packages/auth/events.ts b/packages/auth/events.ts index b1222abb7..2593c418b 100644 --- a/packages/auth/events.ts +++ b/packages/auth/events.ts @@ -2,15 +2,17 @@ import { cookies } from "next/headers"; import dayjs from "dayjs"; import type { NextAuthConfig } from "next-auth"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { and, eq, inArray } from "@homarr/db"; import type { Database } from "@homarr/db"; import { groupMembers, groups, users } from "@homarr/db/schema"; import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { env } from "./env"; import { extractProfileName } from "./providers/oidc/oidc-provider"; +const logger = createLogger({ module: "authEvents" }); + export const createSignInEventHandler = (db: Database): Exclude["signIn"] => { return async ({ user, profile }) => { logger.debug(`SignIn EventHandler for user: ${JSON.stringify(user)} . profile: ${JSON.stringify(profile)}`); @@ -43,9 +45,11 @@ export const createSignInEventHandler = (db: Database): Exclude 0) { - logger.debug( - `Homarr does not have the user in certain groups. user=${userId} count=${missingExternalGroupsForUser.length}`, - ); + logger.debug("Homarr does not have the user in certain groups.", { + user: userId, + count: missingExternalGroupsForUser.length, + }); const groupIds = await db.query.groups.findMany({ columns: { @@ -129,7 +138,10 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s where: inArray(groups.name, missingExternalGroupsForUser), }); - logger.debug(`Homarr has found groups in the database user is not in. user=${userId} count=${groupIds.length}`); + logger.debug("Homarr has found groups in the database user is not in.", { + user: userId, + count: groupIds.length, + }); if (groupIds.length > 0) { await db.insert(groupMembers).values( @@ -139,9 +151,9 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s })), ); - logger.info(`Added user to groups successfully. user=${userId} count=${groupIds.length}`); + logger.info("Added user to groups successfully.", { user: userId, count: groupIds.length }); } else { - logger.debug(`User is already in all groups of Homarr. user=${userId}`); + logger.debug("User is already in all groups of Homarr.", { user: userId }); } } @@ -154,9 +166,10 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s ); if (groupsUserIsNoLongerMemberOfExternally.length > 0) { - logger.debug( - `Homarr has the user in certain groups that LDAP does not have. user=${userId} count=${groupsUserIsNoLongerMemberOfExternally.length}`, - ); + logger.debug("Homarr has the user in certain groups that LDAP does not have.", { + user: userId, + count: groupsUserIsNoLongerMemberOfExternally.length, + }); await db.delete(groupMembers).where( and( @@ -168,8 +181,9 @@ const synchronizeGroupsWithExternalForUserAsync = async (db: Database, userId: s ), ); - logger.info( - `Removed user from groups successfully. user=${userId} count=${groupsUserIsNoLongerMemberOfExternally.length}`, - ); + logger.info("Removed user from groups successfully.", { + user: userId, + count: groupsUserIsNoLongerMemberOfExternally.length, + }); } }; diff --git a/packages/auth/package.json b/packages/auth/package.json index 35e78987b..375cfef08 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -30,7 +30,6 @@ "@homarr/core": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", - "@homarr/log": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "bcrypt": "^6.0.0", "cookies": "^0.9.1", diff --git a/packages/auth/providers/credentials/authorization/basic-authorization.ts b/packages/auth/providers/credentials/authorization/basic-authorization.ts index 544441a9d..04253d837 100644 --- a/packages/auth/providers/credentials/authorization/basic-authorization.ts +++ b/packages/auth/providers/credentials/authorization/basic-authorization.ts @@ -1,12 +1,14 @@ import bcrypt from "bcrypt"; import type { z } from "zod/v4"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { Database } from "@homarr/db"; import { and, eq } from "@homarr/db"; import { users } from "@homarr/db/schema"; -import { logger } from "@homarr/log"; import type { userSignInSchema } from "@homarr/validation/user"; +const logger = createLogger({ module: "basicAuthorization" }); + export const authorizeWithBasicCredentialsAsync = async ( db: Database, credentials: z.infer, @@ -16,19 +18,19 @@ export const authorizeWithBasicCredentialsAsync = async ( }); if (!user?.password) { - logger.info(`user ${credentials.name} was not found`); + logger.info("User not found", { userName: credentials.name }); return null; } - logger.info(`user ${user.name} is trying to log in. checking password...`); + logger.info("User is trying to log in. Checking password...", { userName: user.name }); const isValidPassword = await bcrypt.compare(credentials.password, user.password); if (!isValidPassword) { - logger.warn(`password for user ${user.name} was incorrect`); + logger.warn("Password for user was incorrect", { userName: user.name }); return null; } - logger.info(`user ${user.name} successfully authorized`); + logger.info("User successfully authorized", { userName: user.name }); return { id: user.id, diff --git a/packages/auth/providers/credentials/authorization/ldap-authorization.ts b/packages/auth/providers/credentials/authorization/ldap-authorization.ts index 22dd15181..592fde682 100644 --- a/packages/auth/providers/credentials/authorization/ldap-authorization.ts +++ b/packages/auth/providers/credentials/authorization/ldap-authorization.ts @@ -1,21 +1,23 @@ import { CredentialsSignin } from "@auth/core/errors"; import { z } from "zod/v4"; -import { createId, extractErrorMessage } from "@homarr/common"; +import { createId } from "@homarr/common"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { Database, InferInsertModel } from "@homarr/db"; import { and, eq } from "@homarr/db"; import { users } from "@homarr/db/schema"; -import { logger } from "@homarr/log"; import type { ldapSignInSchema } from "@homarr/validation/user"; import { env } from "../../../env"; import { LdapClient } from "../ldap-client"; +const logger = createLogger({ module: "ldapAuthorization" }); + export const authorizeWithLdapCredentialsAsync = async ( db: Database, credentials: z.infer, ) => { - logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`); + logger.info("User is trying to log in using LDAP. Connecting to LDAP server...", { userName: credentials.name }); const client = new LdapClient(); await client .bindAsync({ @@ -23,8 +25,7 @@ export const authorizeWithLdapCredentialsAsync = async ( password: env.AUTH_LDAP_BIND_PASSWORD, }) .catch((error) => { - logger.error(`Failed to connect to LDAP server ${extractErrorMessage(error)}`); - throw new CredentialsSignin(); + throw new CredentialsSignin("Failed to connect to LDAP server", { cause: error }); }); logger.info("Connected to LDAP server. Searching for user..."); @@ -48,21 +49,21 @@ export const authorizeWithLdapCredentialsAsync = async ( }); if (!ldapUser) { - logger.warn(`User ${credentials.name} not found in LDAP`); - throw new CredentialsSignin(); + throw new CredentialsSignin(`User not found in LDAP username="${credentials.name}"`); } // Validate email const mailResult = await z.string().email().safeParseAsync(ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE]); if (!mailResult.success) { - logger.error( - `User ${credentials.name} found but with invalid or non-existing Email. Not Supported: "${ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE]}"`, - ); - throw new CredentialsSignin(); + logger.error("User found in LDAP but with invalid or non-existing Email", { + userName: credentials.name, + emailValue: ldapUser[env.AUTH_LDAP_USER_MAIL_ATTRIBUTE], + }); + throw new CredentialsSignin("User found in LDAP but with invalid or non-existing Email"); } - logger.info(`User ${credentials.name} found in LDAP. Logging in...`); + logger.info("User found in LDAP. Logging in...", { userName: credentials.name }); // Bind with user credentials to check if the password is correct const userClient = new LdapClient(); @@ -72,12 +73,12 @@ export const authorizeWithLdapCredentialsAsync = async ( password: credentials.password, }) .catch(() => { - logger.warn(`Wrong credentials for user ${credentials.name}`); + logger.warn("Wrong credentials for user", { userName: credentials.name }); throw new CredentialsSignin(); }); await userClient.disconnectAsync(); - logger.info(`User ${credentials.name} logged in successfully, retrieving user groups...`); + logger.info("User credentials are correct. Retrieving user groups...", { userName: credentials.name }); const userGroups = await client .searchAsync({ @@ -93,7 +94,7 @@ export const authorizeWithLdapCredentialsAsync = async ( }) .then((entries) => entries.map((entry) => entry.cn).filter((group): group is string => group !== undefined)); - logger.info(`Found ${userGroups.length} groups for user ${credentials.name}.`); + logger.info("User groups retrieved", { userName: credentials.name, groups: userGroups.length }); await client.disconnectAsync(); @@ -111,7 +112,7 @@ export const authorizeWithLdapCredentialsAsync = async ( }); if (!user) { - logger.info(`User ${credentials.name} not found in the database. Creating...`); + logger.info("User not found in the database. Creating...", { userName: credentials.name }); const insertUser = { id: createId(), @@ -126,7 +127,7 @@ export const authorizeWithLdapCredentialsAsync = async ( user = insertUser; - logger.info(`User ${credentials.name} created successfully.`); + logger.info("User created successfully", { userName: credentials.name }); } return { diff --git a/packages/common/package.json b/packages/common/package.json index cdd59f00f..9db767612 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -28,7 +28,6 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/core": "workspace:^0.1.0", - "@homarr/log": "workspace:^0.1.0", "@paralleldrive/cuid2": "^3.1.0", "dayjs": "^1.11.19", "dns-caching": "^0.2.9", diff --git a/packages/common/src/dns.ts b/packages/common/src/dns.ts index 7a8578b2b..80fde6e1a 100644 --- a/packages/common/src/dns.ts +++ b/packages/common/src/dns.ts @@ -1,6 +1,6 @@ import { DnsCacheManager } from "dns-caching"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { env } from "../env"; @@ -12,6 +12,8 @@ declare global { }; } +const logger = createLogger({ module: "dns" }); + // Initialize global.homarr if not present // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition global.homarr ??= {}; diff --git a/packages/common/src/errors/http/handlers/axios-http-error-handler.ts b/packages/common/src/errors/http/handlers/axios-http-error-handler.ts index 471be713f..200d2cd0b 100644 --- a/packages/common/src/errors/http/handlers/axios-http-error-handler.ts +++ b/packages/common/src/errors/http/handlers/axios-http-error-handler.ts @@ -1,7 +1,5 @@ import { AxiosError } from "axios"; -import { logger } from "@homarr/log"; - import type { AnyRequestError } from "../request-error"; import { RequestError } from "../request-error"; import { ResponseError } from "../response-error"; @@ -9,11 +7,15 @@ import { matchErrorCode } from "./fetch-http-error-handler"; import { HttpErrorHandler } from "./http-error-handler"; export class AxiosHttpErrorHandler extends HttpErrorHandler { + constructor() { + super("axios"); + } + handleRequestError(error: unknown): AnyRequestError | undefined { if (!(error instanceof AxiosError)) return undefined; if (error.code === undefined) return undefined; - logger.debug("Received Axios request error", { + this.logRequestError({ code: error.code, message: error.message, }); @@ -28,8 +30,7 @@ export class AxiosHttpErrorHandler extends HttpErrorHandler { handleResponseError(error: unknown): ResponseError | undefined { if (!(error instanceof AxiosError)) return undefined; if (error.response === undefined) return undefined; - - logger.debug("Received Axios response error", { + this.logResponseError({ status: error.response.status, url: error.response.config.url, message: error.message, diff --git a/packages/common/src/errors/http/handlers/fetch-http-error-handler.ts b/packages/common/src/errors/http/handlers/fetch-http-error-handler.ts index db5d7b9fb..f9e3fb1cc 100644 --- a/packages/common/src/errors/http/handlers/fetch-http-error-handler.ts +++ b/packages/common/src/errors/http/handlers/fetch-http-error-handler.ts @@ -1,5 +1,3 @@ -import { logger } from "@homarr/log"; - import { objectEntries } from "../../../object"; import type { Modify } from "../../../types"; import type { AnyRequestError, AnyRequestErrorInput, RequestErrorCode, RequestErrorReason } from "../request-error"; @@ -9,13 +7,13 @@ import { HttpErrorHandler } from "./http-error-handler"; export class FetchHttpErrorHandler extends HttpErrorHandler { constructor(private type = "undici") { - super(); + super(type); } handleRequestError(error: unknown): AnyRequestError | undefined { if (!isTypeErrorWithCode(error)) return undefined; - logger.debug(`Received ${this.type} request error`, { + this.logRequestError({ code: error.cause.code, }); diff --git a/packages/common/src/errors/http/handlers/http-error-handler.ts b/packages/common/src/errors/http/handlers/http-error-handler.ts index 4195e0dd5..2773b67cb 100644 --- a/packages/common/src/errors/http/handlers/http-error-handler.ts +++ b/packages/common/src/errors/http/handlers/http-error-handler.ts @@ -1,7 +1,24 @@ +import type { Logger } from "@homarr/core/infrastructure/logs"; +import { createLogger } from "@homarr/core/infrastructure/logs"; + import type { AnyRequestError } from "../request-error"; import type { ResponseError } from "../response-error"; export abstract class HttpErrorHandler { + protected logger: Logger; + + constructor(type: string) { + this.logger = createLogger({ module: "httpErrorHandler", type }); + } + + protected logRequestError(metadata: T) { + this.logger.debug("Received request error", metadata); + } + + protected logResponseError(metadata: T) { + this.logger.debug("Received response error", metadata); + } + abstract handleRequestError(error: unknown): AnyRequestError | undefined; abstract handleResponseError(error: unknown): ResponseError | undefined; } diff --git a/packages/common/src/errors/http/handlers/node-fetch-http-error-handler.ts b/packages/common/src/errors/http/handlers/node-fetch-http-error-handler.ts index facfc5a91..4f70395de 100644 --- a/packages/common/src/errors/http/handlers/node-fetch-http-error-handler.ts +++ b/packages/common/src/errors/http/handlers/node-fetch-http-error-handler.ts @@ -1,7 +1,5 @@ import { FetchError } from "node-fetch"; -import { logger } from "@homarr/log"; - import { RequestError } from "../request-error"; import type { AnyRequestError } from "../request-error"; import type { ResponseError } from "../response-error"; @@ -15,14 +13,14 @@ import { HttpErrorHandler } from "./http-error-handler"; */ export class NodeFetchHttpErrorHandler extends HttpErrorHandler { constructor(private type = "node-fetch") { - super(); + super(type); } handleRequestError(error: unknown): AnyRequestError | undefined { if (!(error instanceof FetchError)) return undefined; if (error.code === undefined) return undefined; - logger.debug(`Received ${this.type} request error`, { + this.logRequestError({ code: error.code, message: error.message, }); diff --git a/packages/common/src/errors/http/handlers/octokit-http-error-handler.ts b/packages/common/src/errors/http/handlers/octokit-http-error-handler.ts index 3298770c2..6f8bb0478 100644 --- a/packages/common/src/errors/http/handlers/octokit-http-error-handler.ts +++ b/packages/common/src/errors/http/handlers/octokit-http-error-handler.ts @@ -5,6 +5,10 @@ import { ResponseError } from "../response-error"; import { HttpErrorHandler } from "./http-error-handler"; export class OctokitHttpErrorHandler extends HttpErrorHandler { + constructor() { + super("octokit"); + } + /** * I wasn't able to get a request error triggered. Therefore we ignore them for now * and just forward them as unknown errors @@ -16,6 +20,11 @@ export class OctokitHttpErrorHandler extends HttpErrorHandler { handleResponseError(error: unknown): ResponseError | undefined { if (!(error instanceof OctokitRequestError)) return undefined; + this.logResponseError({ + status: error.status, + url: error.response?.url, + }); + return new ResponseError({ status: error.status, url: error.response?.url, diff --git a/packages/common/src/errors/http/handlers/ofetch-http-error-handler.ts b/packages/common/src/errors/http/handlers/ofetch-http-error-handler.ts index 22d79ad2c..bc347e5a8 100644 --- a/packages/common/src/errors/http/handlers/ofetch-http-error-handler.ts +++ b/packages/common/src/errors/http/handlers/ofetch-http-error-handler.ts @@ -1,7 +1,5 @@ import { FetchError } from "ofetch"; -import { logger } from "@homarr/log"; - import type { AnyRequestError } from "../request-error"; import { ResponseError } from "../response-error"; import { FetchHttpErrorHandler } from "./fetch-http-error-handler"; @@ -14,6 +12,10 @@ import { HttpErrorHandler } from "./http-error-handler"; * It is for example used within the ctrl packages like qbittorrent, deluge, transmission, etc. */ export class OFetchHttpErrorHandler extends HttpErrorHandler { + constructor() { + super("ofetch"); + } + handleRequestError(error: unknown): AnyRequestError | undefined { if (!(error instanceof FetchError)) return undefined; if (!(error.cause instanceof TypeError)) return undefined; @@ -28,7 +30,7 @@ export class OFetchHttpErrorHandler extends HttpErrorHandler { if (!(error instanceof FetchError)) return undefined; if (error.response === undefined) return undefined; - logger.debug("Received ofetch response error", { + this.logResponseError({ status: error.response.status, url: error.response.url, }); diff --git a/packages/common/src/errors/http/handlers/tsdav-http-error-handler.ts b/packages/common/src/errors/http/handlers/tsdav-http-error-handler.ts index 76817ce0f..6fb12c759 100644 --- a/packages/common/src/errors/http/handlers/tsdav-http-error-handler.ts +++ b/packages/common/src/errors/http/handlers/tsdav-http-error-handler.ts @@ -1,11 +1,13 @@ -import { logger } from "@homarr/log"; - import type { AnyRequestError } from "../request-error"; import { ResponseError } from "../response-error"; import { HttpErrorHandler } from "./http-error-handler"; import { NodeFetchHttpErrorHandler } from "./node-fetch-http-error-handler"; export class TsdavHttpErrorHandler extends HttpErrorHandler { + constructor() { + super("tsdav"); + } + handleRequestError(error: unknown): AnyRequestError | undefined { return new NodeFetchHttpErrorHandler("tsdav").handleRequestError(error); } @@ -16,8 +18,9 @@ export class TsdavHttpErrorHandler extends HttpErrorHandler { // https://github.com/natelindev/tsdav/blob/bf33f04b1884694d685ee6f2b43fe9354b12d167/src/account.ts#L86 if (error.message !== "Invalid credentials") return undefined; - logger.debug("Received tsdav response error", { + this.logResponseError({ status: 401, + url: undefined, }); return new ResponseError({ status: 401, url: "?" }); diff --git a/packages/common/src/errors/parse/handlers/json-parse-error-handler.ts b/packages/common/src/errors/parse/handlers/json-parse-error-handler.ts index b0b5581a8..e8bb25ec6 100644 --- a/packages/common/src/errors/parse/handlers/json-parse-error-handler.ts +++ b/packages/common/src/errors/parse/handlers/json-parse-error-handler.ts @@ -1,13 +1,15 @@ -import { logger } from "@homarr/log"; - import { ParseError } from "../parse-error"; import { ParseErrorHandler } from "./parse-error-handler"; export class JsonParseErrorHandler extends ParseErrorHandler { + constructor() { + super("json"); + } + handleParseError(error: unknown): ParseError | undefined { if (!(error instanceof SyntaxError)) return undefined; - logger.debug("Received JSON parse error", { + this.logParseError({ message: error.message, }); diff --git a/packages/common/src/errors/parse/handlers/parse-error-handler.ts b/packages/common/src/errors/parse/handlers/parse-error-handler.ts index 7b19b579d..6231362f5 100644 --- a/packages/common/src/errors/parse/handlers/parse-error-handler.ts +++ b/packages/common/src/errors/parse/handlers/parse-error-handler.ts @@ -1,5 +1,17 @@ +import type { Logger } from "@homarr/core/infrastructure/logs"; +import { createLogger } from "@homarr/core/infrastructure/logs"; + import type { ParseError } from "../parse-error"; export abstract class ParseErrorHandler { + protected logger: Logger; + constructor(type: string) { + this.logger = createLogger({ module: "parseErrorHandler", type }); + } + + protected logParseError(metadata?: Record) { + this.logger.debug("Received parse error", metadata); + } + abstract handleParseError(error: unknown): ParseError | undefined; } diff --git a/packages/common/src/errors/parse/handlers/zod-parse-error-handler.ts b/packages/common/src/errors/parse/handlers/zod-parse-error-handler.ts index 9f581ba99..2146ebb02 100644 --- a/packages/common/src/errors/parse/handlers/zod-parse-error-handler.ts +++ b/packages/common/src/errors/parse/handlers/zod-parse-error-handler.ts @@ -1,12 +1,14 @@ import { fromError } from "zod-validation-error"; import { ZodError } from "zod/v4"; -import { logger } from "@homarr/log"; - import { ParseError } from "../parse-error"; import { ParseErrorHandler } from "./parse-error-handler"; export class ZodParseErrorHandler extends ParseErrorHandler { + constructor() { + super("zod"); + } + handleParseError(error: unknown): ParseError | undefined { if (!(error instanceof ZodError)) return undefined; @@ -17,7 +19,7 @@ export class ZodParseErrorHandler extends ParseErrorHandler { prefix: null, }).toString(); - logger.debug("Received Zod parse error"); + this.logParseError(); return new ParseError(message, { cause: error }); } diff --git a/packages/common/src/fetch-agent.ts b/packages/common/src/fetch-agent.ts index ca670cd36..05afc1abc 100644 --- a/packages/common/src/fetch-agent.ts +++ b/packages/common/src/fetch-agent.ts @@ -1,11 +1,13 @@ import type { Dispatcher } from "undici"; import { Agent } from "undici"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; // The below import statement initializes dns-caching import "./dns"; +const logger = createLogger({ module: "fetchAgent" }); + export class LoggingAgent extends Agent { constructor(...props: ConstructorParameters) { super(...props); diff --git a/packages/common/src/test/fetch-agent.spec.ts b/packages/common/src/test/fetch-agent.spec.ts index 9617a9587..883632f3b 100644 --- a/packages/common/src/test/fetch-agent.spec.ts +++ b/packages/common/src/test/fetch-agent.spec.ts @@ -1,7 +1,7 @@ import type { Dispatcher } from "undici"; import { describe, expect, test, vi } from "vitest"; -import { logger } from "@homarr/log"; +import * as logs from "@homarr/core/infrastructure/logs"; import { LoggingAgent } from "../fetch-agent"; @@ -16,24 +16,36 @@ vi.mock("undici", () => { }; }); +vi.mock("@homarr/core/infrastructure/logs", async () => { + const actual: typeof logs = await vi.importActual("@homarr/core/infrastructure/logs"); + return { + ...actual, + createLogger: vi.fn().mockReturnValue({ + debug: vi.fn(), + }), + }; +}); + const REDACTED = "REDACTED"; +const loggerMock = logs.createLogger({ module: "test" }); + describe("LoggingAgent should log all requests", () => { test("should log all requests", () => { // Arrange - const infoLogSpy = vi.spyOn(logger, "debug"); + const debugSpy = vi.spyOn(loggerMock, "debug"); const agent = new LoggingAgent(); // Act agent.dispatch({ origin: "https://homarr.dev", path: "/", method: "GET" }, {}); // Assert - expect(infoLogSpy).toHaveBeenCalledWith("Dispatching request https://homarr.dev/ (0 headers)"); + expect(debugSpy).toHaveBeenCalledWith("Dispatching request https://homarr.dev/ (0 headers)"); }); test("should show amount of headers", () => { // Arrange - const infoLogSpy = vi.spyOn(logger, "debug"); + const debugSpy = vi.spyOn(loggerMock, "debug"); const agent = new LoggingAgent(); // Act @@ -51,7 +63,7 @@ describe("LoggingAgent should log all requests", () => { ); // Assert - expect(infoLogSpy).toHaveBeenCalledWith(expect.stringContaining("(2 headers)")); + expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining("(2 headers)")); }); test.each([ @@ -69,14 +81,14 @@ describe("LoggingAgent should log all requests", () => { [`/${"a".repeat(32)}/?param=123`, `/${REDACTED}/?param=123`], ])("should redact sensitive data in url https://homarr.dev%s", (path, expected) => { // Arrange - const infoLogSpy = vi.spyOn(logger, "debug"); + const debugSpy = vi.spyOn(loggerMock, "debug"); const agent = new LoggingAgent(); // Act agent.dispatch({ origin: "https://homarr.dev", path, method: "GET" }, {}); // Assert - expect(infoLogSpy).toHaveBeenCalledWith(expect.stringContaining(` https://homarr.dev${expected} `)); + expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining(` https://homarr.dev${expected} `)); }); test.each([ ["empty", "/?empty"], @@ -88,13 +100,13 @@ describe("LoggingAgent should log all requests", () => { ["date times", "/?datetime=2022-01-01T00:00:00.000Z"], ])("should not redact values that are %s", (_reason, path) => { // Arrange - const infoLogSpy = vi.spyOn(logger, "debug"); + const debugSpy = vi.spyOn(loggerMock, "debug"); const agent = new LoggingAgent(); // Act agent.dispatch({ origin: "https://homarr.dev", path, method: "GET" }, {}); // Assert - expect(infoLogSpy).toHaveBeenCalledWith(expect.stringContaining(` https://homarr.dev${path} `)); + expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining(` https://homarr.dev${path} `)); }); }); diff --git a/packages/core/package.json b/packages/core/package.json index aced00b5b..f405b7cc9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -7,7 +7,10 @@ "exports": { "./infrastructure/redis": "./src/infrastructure/redis/client.ts", "./infrastructure/env": "./src/infrastructure/env/index.ts", - ".": "./src/index.ts" + "./infrastructure/logs": "./src/infrastructure/logs/index.ts", + "./infrastructure/logs/constants": "./src/infrastructure/logs/constants.ts", + "./infrastructure/logs/env": "./src/infrastructure/logs/env.ts", + "./infrastructure/logs/error": "./src/infrastructure/logs/error.ts" }, "typesVersions": { "*": { @@ -26,6 +29,8 @@ "dependencies": { "@t3-oss/env-nextjs": "^0.13.8", "ioredis": "5.8.2", + "superjson": "2.2.6", + "winston": "3.19.0", "zod": "^4.1.13" }, "devDependencies": { diff --git a/packages/log/src/constants.ts b/packages/core/src/infrastructure/logs/constants.ts similarity index 100% rename from packages/log/src/constants.ts rename to packages/core/src/infrastructure/logs/constants.ts diff --git a/packages/core/src/infrastructure/logs/env.ts b/packages/core/src/infrastructure/logs/env.ts new file mode 100644 index 000000000..5e8a744b6 --- /dev/null +++ b/packages/core/src/infrastructure/logs/env.ts @@ -0,0 +1,11 @@ +import { z } from "zod/v4"; + +import { createEnv, runtimeEnvWithPrefix } from "../env"; +import { logLevels } from "./constants"; + +export const logsEnv = createEnv({ + server: { + LEVEL: z.enum(logLevels).default("info"), + }, + runtimeEnv: runtimeEnvWithPrefix("LOG_"), +}); diff --git a/packages/core/src/infrastructure/logs/error.ts b/packages/core/src/infrastructure/logs/error.ts new file mode 100644 index 000000000..6224bd922 --- /dev/null +++ b/packages/core/src/infrastructure/logs/error.ts @@ -0,0 +1,9 @@ +export class ErrorWithMetadata extends Error { + public metadata: Record; + + constructor(message: string, metadata: Record = {}, options?: ErrorOptions) { + super(message, options); + this.name = "Error"; + this.metadata = metadata; + } +} diff --git a/packages/log/src/error.ts b/packages/core/src/infrastructure/logs/format/error.ts similarity index 52% rename from packages/log/src/error.ts rename to packages/core/src/infrastructure/logs/format/error.ts index de12da32f..2e5181a4f 100644 --- a/packages/log/src/error.ts +++ b/packages/core/src/infrastructure/logs/format/error.ts @@ -1,5 +1,10 @@ +import { logsEnv } from "../env"; import { formatMetadata } from "./metadata"; +const ERROR_OBJECT_PRUNE_DEPTH = logsEnv.LEVEL === "debug" ? 10 : 3; +const ERROR_STACK_LINE_LIMIT = logsEnv.LEVEL === "debug" ? undefined : 5; +const ERROR_CAUSE_DEPTH = logsEnv.LEVEL === "debug" ? 10 : 5; + /** * Formats the cause of an error in the format * @example caused by Error: {message} @@ -10,7 +15,7 @@ import { formatMetadata } from "./metadata"; */ export const formatErrorCause = (cause: unknown, iteration = 0): string => { // Prevent infinite recursion - if (iteration > 5) { + if (iteration > ERROR_CAUSE_DEPTH) { return ""; } @@ -22,8 +27,12 @@ export const formatErrorCause = (cause: unknown, iteration = 0): string => { return `\ncaused by ${formatErrorTitle(cause)}\n${formatErrorStack(cause.stack)}${formatErrorCause(cause.cause, iteration + 1)}`; } - if (cause instanceof Object) { - return `\ncaused by ${JSON.stringify(cause)}`; + if (typeof cause === "object" && cause !== null) { + if ("cause" in cause) { + const { cause: innerCause, ...rest } = cause; + return `\ncaused by ${JSON.stringify(prune(rest, ERROR_OBJECT_PRUNE_DEPTH))}${formatErrorCause(innerCause, iteration + 1)}`; + } + return `\ncaused by ${JSON.stringify(prune(cause, ERROR_OBJECT_PRUNE_DEPTH))}`; } return `\ncaused by ${cause as string}`; @@ -50,5 +59,28 @@ export const formatErrorTitle = (error: Error) => { * @param stack stack trace * @returns formatted stack trace */ -export const formatErrorStack = (stack: string | undefined) => (stack ? removeFirstLine(stack) : ""); -const removeFirstLine = (stack: string) => stack.split("\n").slice(1).join("\n"); +export const formatErrorStack = (stack: string | undefined) => + stack + ?.split("\n") + .slice(1, ERROR_STACK_LINE_LIMIT ? ERROR_STACK_LINE_LIMIT + 1 : undefined) + .join("\n") ?? ""; + +/** + * Removes nested properties from an object beyond a certain depth + */ +const prune = (value: unknown, depth: number): unknown => { + if (typeof value !== "object" || value === null) { + return value; + } + + if (Array.isArray(value)) { + if (depth === 0) return []; + return value.map((item) => prune(item, depth - 1)); + } + + if (depth === 0) { + return {}; + } + + return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, prune(val, depth - 1)])); +}; diff --git a/packages/core/src/infrastructure/logs/format/index.ts b/packages/core/src/infrastructure/logs/format/index.ts new file mode 100644 index 000000000..1d86ceb01 --- /dev/null +++ b/packages/core/src/infrastructure/logs/format/index.ts @@ -0,0 +1,25 @@ +import { format } from "winston"; + +import { formatErrorCause, formatErrorStack } from "./error"; +import { formatMetadata } from "./metadata"; + +export const logFormat = format.combine( + format.colorize(), + format.timestamp(), + format.errors({ stack: true, cause: true }), + format.printf(({ level, message, timestamp, cause, stack, ...metadata }) => { + const firstLine = `${timestamp as string} ${level}: ${message as string} ${formatMetadata(metadata)}`; + + if (!cause && !stack) { + return firstLine; + } + + const formatedStack = formatErrorStack(stack as string | undefined); + + if (!cause) { + return `${firstLine}\n${formatedStack}`; + } + + return `${firstLine}\n${formatedStack}${formatErrorCause(cause)}`; + }), +); diff --git a/packages/log/src/metadata.ts b/packages/core/src/infrastructure/logs/format/metadata.ts similarity index 51% rename from packages/log/src/metadata.ts rename to packages/core/src/infrastructure/logs/format/metadata.ts index 089a14caa..057aa157d 100644 --- a/packages/log/src/metadata.ts +++ b/packages/core/src/infrastructure/logs/format/metadata.ts @@ -1,7 +1,11 @@ +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; + export const formatMetadata = (metadata: Record | Error, ignoreKeys?: string[]) => { - const filteredMetadata = Object.keys(metadata) + const metadataObject = metadata instanceof ErrorWithMetadata ? metadata.metadata : metadata; + + const filteredMetadata = Object.keys(metadataObject) .filter((key) => !ignoreKeys?.includes(key)) - .map((key) => ({ key, value: metadata[key as keyof typeof metadata] })) + .map((key) => ({ key, value: metadataObject[key as keyof typeof metadataObject] })) .filter(({ value }) => typeof value !== "object" && typeof value !== "function"); return filteredMetadata.map(({ key, value }) => `${key}="${value as string}"`).join(" "); diff --git a/packages/core/src/infrastructure/logs/index.ts b/packages/core/src/infrastructure/logs/index.ts new file mode 100644 index 000000000..41788262f --- /dev/null +++ b/packages/core/src/infrastructure/logs/index.ts @@ -0,0 +1,18 @@ +import winston from "winston"; + +import { logsEnv } from "./env"; +import { logFormat } from "./format"; +import { logTransports } from "./transports"; + +const logger = winston.createLogger({ + format: logFormat, + transports: logTransports, + level: logsEnv.LEVEL, +}); + +interface DefaultMetadata { + module: string; +} + +export const createLogger = (metadata: DefaultMetadata & Record) => logger.child(metadata); +export type Logger = winston.Logger; diff --git a/packages/core/src/infrastructure/logs/transports/index.ts b/packages/core/src/infrastructure/logs/transports/index.ts new file mode 100644 index 000000000..d9789e917 --- /dev/null +++ b/packages/core/src/infrastructure/logs/transports/index.ts @@ -0,0 +1,21 @@ +import { transports } from "winston"; +import type { transport } from "winston"; + +import { RedisTransport } from "./redis-transport"; + +const getTransports = () => { + const defaultTransports: 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))) { + return defaultTransports.concat( + new RedisTransport({ + level: "debug", + }), + ); + } + + return defaultTransports; +}; + +export const logTransports = getTransports(); diff --git a/packages/log/src/redis-transport.ts b/packages/core/src/infrastructure/logs/transports/redis-transport.ts similarity index 82% rename from packages/log/src/redis-transport.ts rename to packages/core/src/infrastructure/logs/transports/redis-transport.ts index a674803bd..e063f60dd 100644 --- a/packages/log/src/redis-transport.ts +++ b/packages/core/src/infrastructure/logs/transports/redis-transport.ts @@ -1,8 +1,8 @@ import superjson from "superjson"; import Transport from "winston-transport"; -import type { RedisClient } from "@homarr/core/infrastructure/redis"; -import { createRedisClient } from "@homarr/core/infrastructure/redis"; +import type { RedisClient } from "../../redis/client"; +import { createRedisClient } from "../../redis/client"; const messageSymbol = Symbol.for("message"); const levelSymbol = Symbol.for("level"); @@ -13,6 +13,7 @@ const levelSymbol = Symbol.for("level"); // export class RedisTransport extends Transport { private redis: RedisClient | null = null; + public static readonly publishChannel = "pubSub:logging"; /** * Log the info to the Redis channel @@ -27,7 +28,7 @@ export class RedisTransport extends Transport { this.redis .publish( - "pubSub:logging", + RedisTransport.publishChannel, superjson.stringify({ message: info[messageSymbol], level: info[levelSymbol], diff --git a/packages/cron-job-api/package.json b/packages/cron-job-api/package.json index fd5bd8182..988f94322 100644 --- a/packages/cron-job-api/package.json +++ b/packages/cron-job-api/package.json @@ -28,7 +28,6 @@ "@homarr/common": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0", - "@homarr/log": "workspace:^0.1.0", "@tanstack/react-query": "^5.90.12", "@trpc/client": "^11.7.2", "@trpc/server": "^11.7.2", diff --git a/packages/cron-jobs-core/package.json b/packages/cron-jobs-core/package.json index 01b4f86f6..06121a3b3 100644 --- a/packages/cron-jobs-core/package.json +++ b/packages/cron-jobs-core/package.json @@ -25,6 +25,7 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^0.1.0", + "@homarr/core": "workspace:^", "@homarr/db": "workspace:^0.1.0", "node-cron": "^4.2.1" }, diff --git a/packages/cron-jobs-core/src/creator.ts b/packages/cron-jobs-core/src/creator.ts index e21065d9e..c22598295 100644 --- a/packages/cron-jobs-core/src/creator.ts +++ b/packages/cron-jobs-core/src/creator.ts @@ -1,8 +1,8 @@ -import { AxiosError } from "axios"; import { createTask, validate } from "node-cron"; import { Stopwatch } from "@homarr/common"; import type { MaybePromise } from "@homarr/common/types"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { db } from "@homarr/db"; import type { Logger } from "./logger"; @@ -33,33 +33,39 @@ const createCallback = MaybePromise) => { const catchingCallbackAsync = async () => { try { - creatorOptions.logger.logDebug(`The callback of '${name}' cron job started`); + creatorOptions.logger.logDebug("The callback of cron job started", { + name, + }); const stopwatch = new Stopwatch(); await creatorOptions.beforeCallback?.(name); const beforeCallbackTook = stopwatch.getElapsedInHumanWords(); await callback(); const callbackTook = stopwatch.getElapsedInHumanWords(); - creatorOptions.logger.logDebug( - `The callback of '${name}' cron job succeeded (before callback took ${beforeCallbackTook}, callback took ${callbackTook})`, - ); + creatorOptions.logger.logDebug("The callback of cron job succeeded", { + name, + beforeCallbackTook, + callbackTook, + }); const durationInMillis = stopwatch.getElapsedInMilliseconds(); if (durationInMillis > expectedMaximumDurationInMillis) { - creatorOptions.logger.logWarning( - `The callback of '${name}' succeeded but took ${(durationInMillis - expectedMaximumDurationInMillis).toFixed(2)}ms longer than expected (${expectedMaximumDurationInMillis}ms). This may indicate that your network performance, host performance or something else is too slow. If this happens too often, it should be looked into.`, - ); + creatorOptions.logger.logWarning("The callback of cron job took longer than expected", { + name, + durationInMillis, + expectedMaximumDurationInMillis, + }); } await creatorOptions.onCallbackSuccess?.(name); } catch (error) { - // Log AxiosError in a less detailed way to prevent very long output - if (error instanceof AxiosError) { - creatorOptions.logger.logError( - `Failed to run job '${name}': [AxiosError] ${error.message} ${error.response?.status} ${error.response?.config.url}\n${error.stack}`, - ); - } else { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - creatorOptions.logger.logError(`Failed to run job '${name}': ${error}`); - } + creatorOptions.logger.logError( + new ErrorWithMetadata( + "The callback of cron job failed", + { + name, + }, + { cause: error }, + ), + ); await creatorOptions.onCallbackError?.(name, error); } }; @@ -80,21 +86,28 @@ const createCallback = ( defaultCronExpression: TExpression, options: CreateCronJobOptions = { runOnStart: false }, ) => { - creatorOptions.logger.logDebug(`Validating cron expression '${defaultCronExpression}' for job: ${name}`); + creatorOptions.logger.logDebug("Validating cron expression for cron job", { + name, + cronExpression: defaultCronExpression, + }); if (!validate(defaultCronExpression)) { throw new Error(`Invalid cron expression '${defaultCronExpression}' for job '${name}'`); } - creatorOptions.logger.logDebug(`Cron job expression '${defaultCronExpression}' for job ${name} is valid`); + creatorOptions.logger.logDebug("Cron job expression for cron job is valid", { + name, + cronExpression: defaultCronExpression, + }); const returnValue = { withCallback: createCallback(name, defaultCronExpression, options, creatorOptions), diff --git a/packages/cron-jobs-core/src/group.ts b/packages/cron-jobs-core/src/group.ts index 28ae714c8..77cd6057d 100644 --- a/packages/cron-jobs-core/src/group.ts +++ b/packages/cron-jobs-core/src/group.ts @@ -19,11 +19,15 @@ export const createJobGroupCreator = ( options: CreateCronJobGroupCreatorOptions, ) => { return >(jobs: TJobs) => { - options.logger.logDebug(`Creating job group with ${Object.keys(jobs).length} jobs.`); + options.logger.logDebug("Creating job group.", { + jobCount: Object.keys(jobs).length, + }); for (const [key, job] of objectEntries(jobs)) { if (typeof key !== "string" || typeof job.name !== "string") continue; - options.logger.logDebug(`Added job ${job.name} to the job registry.`); + options.logger.logDebug("Registering job in the job registry.", { + name: job.name, + }); jobRegistry.set(key, { ...job, name: job.name, @@ -54,7 +58,9 @@ export const createJobGroupCreator = ( if (!job) return; if (!tasks.has(job.name)) return; - options.logger.logInfo(`Starting schedule cron job ${job.name}.`); + options.logger.logInfo("Starting schedule of cron job.", { + name: job.name, + }); await job.onStartAsync(); await tasks.get(name as string)?.start(); }, @@ -64,7 +70,9 @@ export const createJobGroupCreator = ( continue; } - options.logger.logInfo(`Starting schedule of cron job ${job.name}.`); + options.logger.logInfo("Starting schedule of cron job.", { + name: job.name, + }); await job.onStartAsync(); await tasks.get(job.name)?.start(); } @@ -76,19 +84,25 @@ export const createJobGroupCreator = ( throw new Error(`The job "${job.name}" can not be executed manually.`); } - options.logger.logInfo(`Running schedule cron job ${job.name} manually.`); + options.logger.logInfo("Running schedule cron job manually.", { + name: job.name, + }); await tasks.get(name as string)?.execute(); }, stopAsync: async (name: keyof TJobs) => { const job = jobRegistry.get(name as string); if (!job) return; - options.logger.logInfo(`Stopping schedule cron job ${job.name}.`); + options.logger.logInfo("Stopping schedule of cron job.", { + name: job.name, + }); await tasks.get(name as string)?.stop(); }, stopAllAsync: async () => { for (const job of jobRegistry.values()) { - options.logger.logInfo(`Stopping schedule cron job ${job.name}.`); + options.logger.logInfo("Stopping schedule of cron job.", { + name: job.name, + }); await tasks.get(job.name)?.stop(); } }, diff --git a/packages/cron-jobs-core/src/index.ts b/packages/cron-jobs-core/src/index.ts index 08ed3e75e..ee9563f08 100644 --- a/packages/cron-jobs-core/src/index.ts +++ b/packages/cron-jobs-core/src/index.ts @@ -1,10 +1,9 @@ import type { CreateCronJobCreatorOptions } from "./creator"; import { createCronJobCreator } from "./creator"; import { createJobGroupCreator } from "./group"; -import { ConsoleLogger } from "./logger"; export const createCronJobFunctions = ( - options: CreateCronJobCreatorOptions = { logger: new ConsoleLogger() }, + options: CreateCronJobCreatorOptions, ) => { return { createCronJob: createCronJobCreator(options), diff --git a/packages/cron-jobs-core/src/logger.ts b/packages/cron-jobs-core/src/logger.ts index 73628b31c..ff0c1b699 100644 --- a/packages/cron-jobs-core/src/logger.ts +++ b/packages/cron-jobs-core/src/logger.ts @@ -1,24 +1,7 @@ export interface Logger { - logDebug(message: string): void; - logInfo(message: string): void; + logDebug(message: string, metadata?: Record): void; + logInfo(message: string, metadata?: Record): void; + logError(message: string, metadata?: Record): void; logError(error: unknown): void; - logWarning(message: string): void; -} - -export class ConsoleLogger implements Logger { - public logDebug(message: string) { - console.log(message); - } - - public logInfo(message: string) { - console.log(message); - } - - public logError(error: unknown) { - console.error(error); - } - - public logWarning(message: string) { - console.warn(message); - } + logWarning(message: string, metadata?: Record): void; } diff --git a/packages/cron-jobs/package.json b/packages/cron-jobs/package.json index d3baaa48b..61040d79b 100644 --- a/packages/cron-jobs/package.json +++ b/packages/cron-jobs/package.json @@ -25,13 +25,13 @@ "@homarr/analytics": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", + "@homarr/core": "workspace:^0.1.0", "@homarr/cron-job-status": "workspace:^0.1.0", "@homarr/cron-jobs-core": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", "@homarr/icons": "workspace:^0.1.0", "@homarr/integrations": "workspace:^0.1.0", - "@homarr/log": "workspace:^0.1.0", "@homarr/ping": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", "@homarr/request-handler": "workspace:^0.1.0", diff --git a/packages/cron-jobs/src/jobs/docker.ts b/packages/cron-jobs/src/jobs/docker.ts index 4f19d341c..963f395bf 100644 --- a/packages/cron-jobs/src/jobs/docker.ts +++ b/packages/cron-jobs/src/jobs/docker.ts @@ -1,14 +1,17 @@ import SuperJSON from "superjson"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; import { db, eq } from "@homarr/db"; import { items } from "@homarr/db/schema"; -import { logger } from "@homarr/log"; import { dockerContainersRequestHandler } from "@homarr/request-handler/docker"; import type { WidgetComponentProps } from "../../../widgets"; import { createCronJob } from "../lib"; +const logger = createLogger({ module: "dockerJobs" }); + export const dockerContainersJob = createCronJob("dockerContainers", EVERY_MINUTE).withCallback(async () => { const dockerItems = await db.query.items.findMany({ where: eq(items.kind, "dockerContainers"), @@ -21,7 +24,7 @@ export const dockerContainersJob = createCronJob("dockerContainers", EVERY_MINUT const innerHandler = dockerContainersRequestHandler.handler(options); await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); } catch (error) { - logger.error("Failed to update Docker container status", { item, error }); + logger.error(new ErrorWithMetadata("Failed to update Docker container status", { item }, { cause: error })); } }), ); diff --git a/packages/cron-jobs/src/jobs/icons-updater.ts b/packages/cron-jobs/src/jobs/icons-updater.ts index 64332de51..b48c26ea6 100644 --- a/packages/cron-jobs/src/jobs/icons-updater.ts +++ b/packages/cron-jobs/src/jobs/icons-updater.ts @@ -1,14 +1,16 @@ import { createId, splitToNChunks, Stopwatch } from "@homarr/common"; import { env } from "@homarr/common/env"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { EVERY_WEEK } from "@homarr/cron-jobs-core/expressions"; import type { InferInsertModel } from "@homarr/db"; import { db, handleTransactionsAsync, inArray, sql } from "@homarr/db"; import { iconRepositories, icons } from "@homarr/db/schema"; import { fetchIconsAsync } from "@homarr/icons"; -import { logger } from "@homarr/log"; import { createCronJob } from "../lib"; +const logger = createLogger({ module: "iconsUpdaterJobs" }); + export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, { runOnStart: true, expectedMaximumDurationInMillis: 10 * 1000, @@ -21,9 +23,11 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, { const countIcons = repositoryIconGroups .map((group) => group.icons.length) .reduce((partialSum, arrayLength) => partialSum + arrayLength, 0); - logger.info( - `Successfully fetched ${countIcons} icons from ${repositoryIconGroups.length} repositories within ${stopWatch.getElapsedInHumanWords()}`, - ); + logger.info("Fetched icons from repositories", { + repositoryCount: repositoryIconGroups.length, + iconCount: countIcons, + duration: stopWatch.getElapsedInHumanWords(), + }); const databaseIconRepositories = await db.query.iconRepositories.findMany({ with: { @@ -162,5 +166,9 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, { }, }); - logger.info(`Updated database within ${stopWatch.getElapsedInHumanWords()} (-${countDeleted}, +${countInserted})`); + logger.info("Updated icons in database", { + duration: stopWatch.getElapsedInHumanWords(), + added: countInserted, + deleted: countDeleted, + }); }); diff --git a/packages/cron-jobs/src/jobs/ping.ts b/packages/cron-jobs/src/jobs/ping.ts index 318db80bd..bfe97bbda 100644 --- a/packages/cron-jobs/src/jobs/ping.ts +++ b/packages/cron-jobs/src/jobs/ping.ts @@ -1,12 +1,15 @@ +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; import { db } from "@homarr/db"; import { getServerSettingByKeyAsync } from "@homarr/db/queries"; -import { logger } from "@homarr/log"; import { sendPingRequestAsync } from "@homarr/ping"; import { pingChannel, pingUrlChannel } from "@homarr/redis"; import { createCronJob } from "../lib"; +const logger = createLogger({ module: "pingJobs" }); + const resetPreviousUrlsAsync = async () => { await pingUrlChannel.clearAsync(); logger.info("Cleared previous ping urls"); @@ -31,9 +34,9 @@ const pingAsync = async (url: string) => { const pingResult = await sendPingRequestAsync(url); if ("statusCode" in pingResult) { - logger.debug(`executed ping for url ${url} with status code ${pingResult.statusCode}`); + logger.debug("Executed ping successfully", { url, statusCode: pingResult.statusCode }); } else { - logger.error(`Executing ping for url ${url} failed with error: ${pingResult.error}`); + logger.error(new ErrorWithMetadata("Executing ping failed", { url }, { cause: pingResult.error })); } await pingChannel.publishAsync({ diff --git a/packages/cron-jobs/src/jobs/rss-feeds.ts b/packages/cron-jobs/src/jobs/rss-feeds.ts index 15b883716..2dda83499 100644 --- a/packages/cron-jobs/src/jobs/rss-feeds.ts +++ b/packages/cron-jobs/src/jobs/rss-feeds.ts @@ -1,15 +1,18 @@ import SuperJSON from "superjson"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions"; import { db, eq } from "@homarr/db"; import { items } from "@homarr/db/schema"; -import { logger } from "@homarr/log"; // This import is done that way to avoid circular dependencies. import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds"; import type { WidgetComponentProps } from "../../../widgets"; import { createCronJob } from "../lib"; +const logger = createLogger({ module: "rssFeedsJobs" }); + export const rssFeedsJob = createCronJob("rssFeeds", EVERY_10_MINUTES).withCallback(async () => { const rssItems = await db.query.items.findMany({ where: eq(items.kind, "rssFeed"), @@ -29,7 +32,7 @@ export const rssFeedsJob = createCronJob("rssFeeds", EVERY_10_MINUTES).withCallb forceUpdate: true, }); } catch (error) { - logger.error("Failed to update RSS feed", { url, error }); + logger.error(new ErrorWithMetadata("Failed to update RSS feed", { url }, { cause: error })); } } } diff --git a/packages/cron-jobs/src/jobs/weather.ts b/packages/cron-jobs/src/jobs/weather.ts index 20f96728a..1b23ade4c 100644 --- a/packages/cron-jobs/src/jobs/weather.ts +++ b/packages/cron-jobs/src/jobs/weather.ts @@ -1,14 +1,17 @@ import SuperJSON from "superjson"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions"; import { db, eq } from "@homarr/db"; import { items } from "@homarr/db/schema"; -import { logger } from "@homarr/log"; import { weatherRequestHandler } from "@homarr/request-handler/weather"; import type { WidgetComponentProps } from "../../../widgets"; import { createCronJob } from "../lib"; +const logger = createLogger({ module: "weatherJobs" }); + export const weatherJob = createCronJob("weather", EVERY_10_MINUTES).withCallback(async () => { const weatherItems = await db.query.items.findMany({ where: eq(items.kind, "weather"), @@ -27,7 +30,7 @@ export const weatherJob = createCronJob("weather", EVERY_10_MINUTES).withCallbac }); await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); } catch (error) { - logger.error("Failed to update weather", { id: item.id, error }); + logger.error(new ErrorWithMetadata("Failed to update weather", { id: item.id }, { cause: error })); } } }); diff --git a/packages/cron-jobs/src/lib/index.ts b/packages/cron-jobs/src/lib/index.ts index 28516fc53..898deebb3 100644 --- a/packages/cron-jobs/src/lib/index.ts +++ b/packages/cron-jobs/src/lib/index.ts @@ -1,24 +1,29 @@ +import { createLogger } from "@homarr/core/infrastructure/logs"; import { beforeCallbackAsync, onCallbackErrorAsync, onCallbackSuccessAsync } from "@homarr/cron-job-status/publisher"; import { createCronJobFunctions } from "@homarr/cron-jobs-core"; import type { Logger } from "@homarr/cron-jobs-core/logger"; -import { logger } from "@homarr/log"; import type { TranslationObject } from "@homarr/translation"; +const logger = createLogger({ module: "cronJobs" }); + class WinstonCronJobLogger implements Logger { - logDebug(message: string) { - logger.debug(message); + logDebug(message: string, metadata?: Record): void { + logger.debug(message, metadata); } - - logInfo(message: string) { - logger.info(message); + logInfo(message: string, metadata?: Record): void { + logger.info(message, metadata); } - - logError(error: unknown) { - logger.error(error); + logError(message: string, metadata?: Record): void; + logError(error: unknown): void; + logError(messageOrError: unknown, metadata?: Record): void { + if (typeof messageOrError === "string") { + logger.error(messageOrError, metadata); + return; + } + logger.error(messageOrError); } - - logWarning(message: string) { - logger.warn(message); + logWarning(message: string, metadata?: Record): void { + logger.warn(message, metadata); } } diff --git a/packages/db/driver.ts b/packages/db/driver.ts index 9ed5c899f..7950e1183 100644 --- a/packages/db/driver.ts +++ b/packages/db/driver.ts @@ -11,13 +11,15 @@ import type { Pool as MysqlConnectionPool } from "mysql2"; import mysql from "mysql2"; import { Pool as PostgresPool } from "pg"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { env } from "./env"; import * as mysqlSchema from "./schema/mysql"; import * as pgSchema from "./schema/postgresql"; import * as sqliteSchema from "./schema/sqlite"; +const logger = createLogger({ module: "db" }); + export type HomarrDatabase = BetterSQLite3Database; export type HomarrDatabaseMysql = MySql2Database; export type HomarrDatabasePostgresql = NodePgDatabase; @@ -44,7 +46,7 @@ export let database: HomarrDatabase; class WinstonDrizzleLogger implements Logger { logQuery(query: string, _: unknown[]): void { - logger.debug(`Executed SQL query: ${query}`); + logger.debug("Executed SQL query", { query }); } } diff --git a/packages/db/package.json b/packages/db/package.json index 8a9a1669d..79d8f6b6c 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -47,7 +47,6 @@ "@homarr/common": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", - "@homarr/log": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", "@mantine/core": "^8.3.10", "@paralleldrive/cuid2": "^3.1.0", diff --git a/packages/icons/package.json b/packages/icons/package.json index 682bfa651..884021cc7 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -24,8 +24,8 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^0.1.0", - "@homarr/db": "workspace:^0.1.0", - "@homarr/log": "workspace:^0.1.0" + "@homarr/core": "workspace:^0.1.0", + "@homarr/db": "workspace:^0.1.0" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/icons/src/repositories/icon-repository.ts b/packages/icons/src/repositories/icon-repository.ts index 525ca1422..454df71e6 100644 --- a/packages/icons/src/repositories/icon-repository.ts +++ b/packages/icons/src/repositories/icon-repository.ts @@ -1,8 +1,11 @@ -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import type { IconRepositoryLicense } from "../types/icon-repository-license"; import type { RepositoryIconGroup } from "../types/repository-icon-group"; +const logger = createLogger({ module: "iconRepository" }); + export abstract class IconRepository { protected readonly allowedImageFileTypes = [".png", ".svg", ".jpeg"]; @@ -19,7 +22,9 @@ export abstract class IconRepository { try { return await this.getAllIconsInternalAsync(); } catch (err) { - logger.error(`Unable to request icons from repository "${this.slug}": ${JSON.stringify(err)}`); + logger.error( + new ErrorWithMetadata("Unable to request icons from repository", { slug: this.slug }, { cause: err }), + ); return { success: false, icons: [], diff --git a/packages/image-proxy/package.json b/packages/image-proxy/package.json index e46fda9f9..58d1f2a31 100644 --- a/packages/image-proxy/package.json +++ b/packages/image-proxy/package.json @@ -24,7 +24,7 @@ "dependencies": { "@homarr/certificates": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", - "@homarr/log": "workspace:^0.1.0", + "@homarr/core": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", "bcrypt": "^6.0.0" }, diff --git a/packages/image-proxy/src/index.ts b/packages/image-proxy/src/index.ts index 19f66e608..db3fc1a5b 100644 --- a/packages/image-proxy/src/index.ts +++ b/packages/image-proxy/src/index.ts @@ -3,9 +3,12 @@ import bcrypt from "bcrypt"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { createId } from "@homarr/common"; import { decryptSecret, encryptSecret } from "@homarr/common/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { createGetSetChannel } from "@homarr/redis"; +const logger = createLogger({ module: "imageProxy" }); + const createHashChannel = (hash: `${string}.${string}`) => createGetSetChannel(`image-proxy:hash:${hash}`); const createUrlByIdChannel = (id: string) => createGetSetChannel<{ @@ -25,7 +28,7 @@ export class ImageProxy { } const salt = await bcrypt.genSalt(10); - logger.debug(`Generated new salt for image proxy salt="${salt}"`); + logger.debug("Generated new salt for image proxy", { salt }); ImageProxy.salt = salt; await saltChannel.setAsync(salt); return salt; @@ -34,9 +37,11 @@ export class ImageProxy { public async createImageAsync(url: string, headers?: Record): Promise { const existingId = await this.getExistingIdAsync(url, headers); if (existingId) { - logger.debug( - `Image already exists in the proxy id="${existingId}" url="${this.redactUrl(url)}" headers="${this.redactHeaders(headers ?? null)}"`, - ); + logger.debug("Image already exists in the proxy", { + id: existingId, + url: this.redactUrl(url), + headers: this.redactHeaders(headers ?? null), + }); return this.createImageUrl(existingId); } @@ -59,15 +64,25 @@ export class ImageProxy { const proxyUrl = this.createImageUrl(id); if (!response.ok) { logger.error( - `Failed to fetch image id="${id}" url="${this.redactUrl(urlAndHeaders.url)}" headers="${this.redactHeaders(urlAndHeaders.headers)}" proxyUrl="${proxyUrl}" statusCode="${response.status}"`, + new ErrorWithMetadata("Failed to fetch image", { + id, + url: this.redactUrl(urlAndHeaders.url), + headers: this.redactHeaders(urlAndHeaders.headers), + proxyUrl, + statusCode: response.status, + }), ); return null; } const blob = (await response.blob()) as Blob; - logger.debug( - `Forwarding image succeeded id="${id}" url="${this.redactUrl(urlAndHeaders.url)}" headers="${this.redactHeaders(urlAndHeaders.headers)}" proxyUrl="${proxyUrl} size="${(blob.size / 1024).toFixed(1)}KB"`, - ); + logger.debug("Forwarding image succeeded", { + id, + url: this.redactUrl(urlAndHeaders.url), + headers: this.redactHeaders(urlAndHeaders.headers), + proxyUrl, + size: `${(blob.size / 1024).toFixed(1)}KB`, + }); return blob; } @@ -80,7 +95,7 @@ export class ImageProxy { const urlHeaderChannel = createUrlByIdChannel(id); const urlHeader = await urlHeaderChannel.getAsync(); if (!urlHeader) { - logger.warn(`Image not found in the proxy id="${id}"`); + logger.warn("Image not found in the proxy", { id }); return null; } @@ -112,9 +127,11 @@ export class ImageProxy { }); await hashChannel.setAsync(id); - logger.debug( - `Stored image in the proxy id="${id}" url="${this.redactUrl(url)}" headers="${this.redactHeaders(headers ?? null)}"`, - ); + logger.debug("Stored image in the proxy", { + id, + url: this.redactUrl(url), + headers: this.redactHeaders(headers ?? null), + }); } private redactUrl(url: string): string { diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 401a86133..d1fa4aaf3 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -31,10 +31,10 @@ "@gitbeaker/rest": "^43.8.0", "@homarr/certificates": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", + "@homarr/core": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", "@homarr/image-proxy": "workspace:^0.1.0", - "@homarr/log": "workspace:^0.1.0", "@homarr/node-unifi": "^2.6.0", "@homarr/redis": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", diff --git a/packages/integrations/src/base/errors/decorator.ts b/packages/integrations/src/base/errors/decorator.ts index c5a200162..3aac2eff7 100644 --- a/packages/integrations/src/base/errors/decorator.ts +++ b/packages/integrations/src/base/errors/decorator.ts @@ -1,5 +1,5 @@ import { isFunction } from "@homarr/common"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { Integration } from "../integration"; import type { IIntegrationErrorHandler } from "./handler"; @@ -8,9 +8,7 @@ import { IntegrationError } from "./integration-error"; import { IntegrationUnknownError } from "./integration-unknown-error"; import { integrationJsonParseErrorHandler, integrationZodParseErrorHandler } from "./parse"; -const localLogger = logger.child({ - module: "HandleIntegrationErrors", -}); +const logger = createLogger({ module: "handleIntegrationErrors" }); // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-explicit-any type AbstractConstructor = abstract new (...args: any[]) => T; @@ -59,7 +57,7 @@ export const HandleIntegrationErrors = (errorHandlers: IIntegrationErrorHandler[ } // If the error was handled and should be thrown again, throw it - localLogger.debug("Unhandled error in integration", { + logger.debug("Unhandled error in integration", { error: error instanceof Error ? `${error.name}: ${error.message}` : undefined, integrationName: this.publicIntegration.name, }); diff --git a/packages/integrations/src/base/session-store.ts b/packages/integrations/src/base/session-store.ts index 58e4dad46..468440a0e 100644 --- a/packages/integrations/src/base/session-store.ts +++ b/packages/integrations/src/base/session-store.ts @@ -1,10 +1,11 @@ import superjson from "superjson"; import { decryptSecret, encryptSecret } from "@homarr/common/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { createGetSetChannel } from "@homarr/redis"; -const localLogger = logger.child({ module: "SessionStore" }); +const logger = createLogger({ module: "sessionStore" }); export const createSessionStore = (integration: { id: string }) => { const channelName = `session-store:${integration.id}`; @@ -12,26 +13,26 @@ export const createSessionStore = (integration: { id: string }) => { return { async getAsync() { - localLogger.debug("Getting session from store", { store: channelName }); + logger.debug("Getting session from store", { store: channelName }); const value = await channel.getAsync(); if (!value) return null; try { return superjson.parse(decryptSecret(value)); } catch (error) { - localLogger.warn("Failed to load session", { store: channelName, error }); + logger.warn("Failed to load session", { store: channelName, error }); return null; } }, async setAsync(value: TValue) { - localLogger.debug("Updating session in store", { store: channelName }); + logger.debug("Updating session in store", { store: channelName }); try { await channel.setAsync(encryptSecret(superjson.stringify(value))); } catch (error) { - localLogger.error("Failed to save session", { store: channelName, error }); + logger.error(new ErrorWithMetadata("Failed to save session", { store: channelName }, { cause: error })); } }, async clearAsync() { - localLogger.debug("Cleared session in store", { store: channelName }); + logger.debug("Cleared session in store", { store: channelName }); await channel.removeAsync(); }, }; diff --git a/packages/integrations/src/base/test-connection/test-connection-service.ts b/packages/integrations/src/base/test-connection/test-connection-service.ts index 16bf664b7..bedc603f7 100644 --- a/packages/integrations/src/base/test-connection/test-connection-service.ts +++ b/packages/integrations/src/base/test-connection/test-connection-service.ts @@ -7,7 +7,7 @@ import { getTrustedCertificateHostnamesAsync, } from "@homarr/certificates/server"; import { getPortFromUrl } from "@homarr/common"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationRequestErrorOfType } from "../errors/http/integration-request-error"; import { IntegrationRequestError } from "../errors/http/integration-request-error"; @@ -15,8 +15,8 @@ import { IntegrationError } from "../errors/integration-error"; import type { AnyTestConnectionError } from "./test-connection-error"; import { TestConnectionError } from "./test-connection-error"; -const localLogger = logger.child({ - module: "TestConnectionService", +const logger = createLogger({ + module: "testConnectionService", }); export type TestingResult = @@ -36,7 +36,7 @@ export class TestConnectionService { constructor(private url: URL) {} public async handleAsync(testingCallbackAsync: AsyncTestingCallback) { - localLogger.debug("Testing connection", { + logger.debug("Testing connection", { url: this.url.toString(), }); @@ -72,14 +72,14 @@ export class TestConnectionService { }); if (testingResult.success) { - localLogger.debug("Testing connection succeeded", { + logger.debug("Testing connection succeeded", { url: this.url.toString(), }); return testingResult; } - localLogger.debug("Testing connection failed", { + logger.debug("Testing connection failed", { url: this.url.toString(), error: `${testingResult.error.name}: ${testingResult.error.message}`, }); @@ -124,7 +124,7 @@ export class TestConnectionService { const x509 = socket.getPeerX509Certificate(); socket.destroy(); - localLogger.debug("Fetched certificate", { + logger.debug("Fetched certificate", { url: this.url.toString(), subject: x509?.subject, issuer: x509?.issuer, diff --git a/packages/integrations/src/codeberg/codeberg-integration.ts b/packages/integrations/src/codeberg/codeberg-integration.ts index ea6543cbb..c56aaae37 100644 --- a/packages/integrations/src/codeberg/codeberg-integration.ts +++ b/packages/integrations/src/codeberg/codeberg-integration.ts @@ -1,7 +1,7 @@ import type { RequestInit, Response } from "undici"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; @@ -15,7 +15,7 @@ import type { } from "../interfaces/releases-providers/releases-providers-types"; import { detailsResponseSchema, releasesResponseSchema } from "./codeberg-schemas"; -const localLogger = logger.child({ module: "CodebergIntegration" }); +const logger = createLogger({ module: "codebergIntegration" }); export class CodebergIntegration extends Integration implements ReleasesProviderIntegration { private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise): Promise { @@ -45,10 +45,9 @@ export class CodebergIntegration extends Integration implements ReleasesProvider private parseIdentifier(identifier: string) { const [owner, name] = identifier.split("/"); if (!owner || !name) { - localLogger.warn( - `Invalid identifier format. Expected 'owner/name', for ${identifier} with Codeberg integration`, - { identifier }, - ); + logger.warn("Invalid identifier format. Expected 'owner/name', for identifier", { + identifier, + }); return null; } return { owner, name }; @@ -109,7 +108,7 @@ export class CodebergIntegration extends Integration implements ReleasesProvider }); if (!response.ok) { - localLogger.warn(`Failed to get details response for ${owner}/${name} with Codeberg integration`, { + logger.warn("Failed to get details", { owner, name, error: response.statusText, @@ -122,7 +121,7 @@ export class CodebergIntegration extends Integration implements ReleasesProvider const { data, success, error } = detailsResponseSchema.safeParse(responseJson); if (!success) { - localLogger.warn(`Failed to parse details response for ${owner}/${name} with Codeberg integration`, { + logger.warn("Failed to parse details", { owner, name, error, diff --git a/packages/integrations/src/docker-hub/docker-hub-integration.ts b/packages/integrations/src/docker-hub/docker-hub-integration.ts index d0e99c0e6..3778e69b9 100644 --- a/packages/integrations/src/docker-hub/docker-hub-integration.ts +++ b/packages/integrations/src/docker-hub/docker-hub-integration.ts @@ -2,7 +2,7 @@ import type { fetch, RequestInit, Response } from "undici"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationInput, IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; @@ -18,7 +18,7 @@ import type { } from "../interfaces/releases-providers/releases-providers-types"; import { accessTokenResponseSchema, detailsResponseSchema, releasesResponseSchema } from "./docker-hub-schemas"; -const localLogger = logger.child({ module: "DockerHubIntegration" }); +const logger = createLogger({ module: "dockerHubIntegration" }); export class DockerHubIntegration extends Integration implements ReleasesProviderIntegration { private readonly sessionStore: SessionStore; @@ -35,7 +35,7 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide const storedSession = await this.sessionStore.getAsync(); if (storedSession) { - localLogger.debug("Using stored session for request", { integrationId: this.integration.id }); + logger.debug("Using stored session for request", { integrationId: this.integration.id }); const response = await callback({ Authorization: `Bearer ${storedSession}`, }); @@ -43,7 +43,7 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide return response; } - localLogger.debug("Session expired, getting new session", { integrationId: this.integration.id }); + logger.debug("Session expired, getting new session", { integrationId: this.integration.id }); } const accessToken = await this.getSessionAsync(); @@ -57,10 +57,10 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide const hasAuth = this.hasSecretValue("username") && this.hasSecretValue("personalAccessToken"); if (hasAuth) { - localLogger.debug("Testing DockerHub connection with authentication", { integrationId: this.integration.id }); + logger.debug("Testing DockerHub connection with authentication", { integrationId: this.integration.id }); await this.getSessionAsync(input.fetchAsync); } else { - localLogger.debug("Testing DockerHub connection without authentication", { integrationId: this.integration.id }); + logger.debug("Testing DockerHub connection without authentication", { integrationId: this.integration.id }); const response = await input.fetchAsync(this.url("/v2/repositories/library")); if (!response.ok) { return TestConnectionError.StatusResult(response); @@ -76,7 +76,7 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide if (!identifier.includes("/")) return { owner: "", name: identifier }; const [owner, name] = identifier.split("/"); if (!owner || !name) { - localLogger.warn(`Invalid identifier format. Expected 'owner/name' or 'name', for ${identifier} on DockerHub`, { + logger.warn("Invalid identifier format. Expected 'owner/name' or 'name', for identifier", { identifier, }); return null; @@ -137,7 +137,7 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide }); if (!response.ok) { - localLogger.warn(`Failed to get details response for ${relativeUrl} with DockerHub integration`, { + logger.warn("Failed to get details response", { relativeUrl, error: response.statusText, }); @@ -149,7 +149,7 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide const { data, success, error } = detailsResponseSchema.safeParse(responseJson); if (!success) { - localLogger.warn(`Failed to parse details response for ${relativeUrl} with DockerHub integration`, { + logger.warn("Failed to parse details response", { relativeUrl, error, }); @@ -183,7 +183,7 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide throw new ResponseError({ status: 401, url: response.url }); } - localLogger.info("Received session successfully", { integrationId: this.integration.id }); + logger.info("Received session successfully", { integrationId: this.integration.id }); return result.access_token; } diff --git a/packages/integrations/src/github-container-registry/github-container-registry-integration.ts b/packages/integrations/src/github-container-registry/github-container-registry-integration.ts index 00a7b27c4..777b67287 100644 --- a/packages/integrations/src/github-container-registry/github-container-registry-integration.ts +++ b/packages/integrations/src/github-container-registry/github-container-registry-integration.ts @@ -3,7 +3,7 @@ import { Octokit, RequestError } from "octokit"; import type { fetch } from "undici"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { HandleIntegrationErrors } from "../base/errors/decorator"; import { integrationOctokitHttpErrorHandler } from "../base/errors/http"; @@ -18,7 +18,7 @@ import type { ReleaseResponse, } from "../interfaces/releases-providers/releases-providers-types"; -const localLogger = logger.child({ module: "GitHubContainerRegistryIntegration" }); +const logger = createLogger({ module: "githubContainerRegistryIntegration" }); @HandleIntegrationErrors([integrationOctokitHttpErrorHandler]) export class GitHubContainerRegistryIntegration extends Integration implements ReleasesProviderIntegration { @@ -45,10 +45,7 @@ export class GitHubContainerRegistryIntegration extends Integration implements R private parseIdentifier(identifier: string) { const [owner, name] = identifier.split("/"); if (!owner || !name) { - localLogger.warn( - `Invalid identifier format. Expected 'owner/name', for ${identifier} with GitHub Container Registry integration`, - { identifier }, - ); + logger.warn("Invalid identifier format. Expected 'owner/name', for identifier", { identifier }); return null; } return { owner, name }; @@ -91,7 +88,7 @@ export class GitHubContainerRegistryIntegration extends Integration implements R return { success: true, data: { ...details, ...latestRelease } }; } catch (error) { const errorMessage = error instanceof RequestError ? error.message : String(error); - localLogger.warn(`Failed to get releases for ${owner}\\${name} with GitHub Container Registry integration`, { + logger.warn("Failed to get releases", { owner, name, error: errorMessage, @@ -123,7 +120,7 @@ export class GitHubContainerRegistryIntegration extends Integration implements R forksCount: response.data.repository?.forks_count, }; } catch (error) { - localLogger.warn(`Failed to get details for ${owner}\\${name} with GitHub Container Registry integration`, { + logger.warn("Failed to get details", { owner, name, error: error instanceof RequestError ? error.message : String(error), diff --git a/packages/integrations/src/github/github-integration.ts b/packages/integrations/src/github/github-integration.ts index 65e103c38..3c3825d2b 100644 --- a/packages/integrations/src/github/github-integration.ts +++ b/packages/integrations/src/github/github-integration.ts @@ -3,7 +3,7 @@ import { Octokit, RequestError as OctokitRequestError } from "octokit"; import type { fetch } from "undici"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { HandleIntegrationErrors } from "../base/errors/decorator"; import { integrationOctokitHttpErrorHandler } from "../base/errors/http"; @@ -18,7 +18,7 @@ import type { ReleaseResponse, } from "../interfaces/releases-providers/releases-providers-types"; -const localLogger = logger.child({ module: "GithubIntegration" }); +const logger = createLogger({ module: "githubIntegration" }); @HandleIntegrationErrors([integrationOctokitHttpErrorHandler]) export class GithubIntegration extends Integration implements ReleasesProviderIntegration { @@ -45,7 +45,7 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn private parseIdentifier(identifier: string) { const [owner, name] = identifier.split("/"); if (!owner || !name) { - localLogger.warn(`Invalid identifier format. Expected 'owner/name', for ${identifier} with Github integration`, { + logger.warn("Invalid identifier format. Expected 'owner/name' for identifier", { identifier, }); return null; @@ -64,7 +64,7 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn const releasesResponse = await api.rest.repos.listReleases({ owner, repo: name }); if (releasesResponse.data.length === 0) { - localLogger.warn(`No releases found, for ${owner}/${name} with Github integration`, { + logger.warn("No releases found", { identifier: `${owner}/${name}`, }); return { success: false, error: { code: "noMatchingVersion" } }; @@ -91,7 +91,7 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn return { success: true, data: { ...details, ...latestRelease } }; } catch (error) { const errorMessage = error instanceof OctokitRequestError ? error.message : String(error); - localLogger.warn(`Failed to get releases for ${owner}\\${name} with Github integration`, { + logger.warn("Failed to get releases", { owner, name, error: errorMessage, @@ -122,7 +122,7 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn forksCount: response.data.forks_count, }; } catch (error) { - localLogger.warn(`Failed to get details for ${owner}\\${name} with Github integration`, { + logger.warn("Failed to get details", { owner, name, error: error instanceof OctokitRequestError ? error.message : String(error), diff --git a/packages/integrations/src/gitlab/gitlab-integration.ts b/packages/integrations/src/gitlab/gitlab-integration.ts index c6bad7a42..2c9ecf08d 100644 --- a/packages/integrations/src/gitlab/gitlab-integration.ts +++ b/packages/integrations/src/gitlab/gitlab-integration.ts @@ -4,7 +4,7 @@ import type { FormattedResponse, RequestOptions, ResourceOptions } from "@gitbea import { Gitlab } from "@gitbeaker/rest"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; @@ -18,7 +18,7 @@ import type { ReleaseResponse, } from "../interfaces/releases-providers/releases-providers-types"; -const localLogger = logger.child({ module: "GitlabIntegration" }); +const logger = createLogger({ module: "gitlabIntegration" }); export class GitlabIntegration extends Integration implements ReleasesProviderIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { @@ -48,7 +48,7 @@ export class GitlabIntegration extends Integration implements ReleasesProviderIn }); if (releasesResponse instanceof Error) { - localLogger.warn(`Failed to get releases for ${identifier} with Gitlab integration`, { + logger.warn("No releases found", { identifier, error: releasesResponse.message, }); @@ -78,7 +78,7 @@ export class GitlabIntegration extends Integration implements ReleasesProviderIn return { success: true, data: { ...details, ...latestRelease } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - localLogger.warn(`Failed to get releases for ${identifier} with Gitlab integration`, { + logger.warn("Failed to get releases", { identifier, error: errorMessage, }); @@ -91,7 +91,7 @@ export class GitlabIntegration extends Integration implements ReleasesProviderIn const response = await api.Projects.show(identifier); if (response instanceof Error) { - localLogger.warn(`Failed to get details for ${identifier} with Gitlab integration`, { + logger.warn("Failed to get details", { identifier, error: response.message, }); @@ -100,7 +100,7 @@ export class GitlabIntegration extends Integration implements ReleasesProviderIn } if (!response.web_url) { - localLogger.warn(`No web URL found for ${identifier} with Gitlab integration`, { + logger.warn("No web URL found", { identifier, }); return undefined; @@ -117,7 +117,7 @@ export class GitlabIntegration extends Integration implements ReleasesProviderIn forksCount: response.forks_count, }; } catch (error) { - localLogger.warn(`Failed to get details for ${identifier} with Gitlab integration`, { + logger.warn("Failed to get details", { identifier, error: error instanceof Error ? error.message : String(error), }); diff --git a/packages/integrations/src/homeassistant/homeassistant-integration.ts b/packages/integrations/src/homeassistant/homeassistant-integration.ts index c6a0e1191..096dfc49a 100644 --- a/packages/integrations/src/homeassistant/homeassistant-integration.ts +++ b/packages/integrations/src/homeassistant/homeassistant-integration.ts @@ -2,7 +2,8 @@ import z from "zod"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; @@ -13,13 +14,15 @@ import type { ISmartHomeIntegration } from "../interfaces/smart-home/smart-home- import type { CalendarEvent } from "../types"; import { calendarEventSchema, calendarsSchema, entityStateSchema } from "./homeassistant-types"; +const logger = createLogger({ module: "homeAssistantIntegration" }); + export class HomeAssistantIntegration extends Integration implements ISmartHomeIntegration, ICalendarIntegration { public async getEntityStateAsync(entityId: string) { try { const response = await this.getAsync(`/api/states/${entityId}`); const body = await response.json(); if (!response.ok) { - logger.warn(`Response did not indicate success`); + logger.warn("Response did not indicate success"); return { success: false as const, error: "Response did not indicate success", @@ -27,7 +30,7 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI } return entityStateSchema.safeParseAsync(body); } catch (err) { - logger.error(`Failed to fetch from ${this.url("/")}: ${err as string}`); + logger.error(new ErrorWithMetadata("Failed to fetch entity state", { entityId }, { cause: err })); return { success: false as const, error: err, @@ -43,7 +46,7 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI return response.ok; } catch (err) { - logger.error(`Failed to fetch from '${this.url("/")}': ${err as string}`); + logger.error(new ErrorWithMetadata("Failed to trigger automation", { entityId }, { cause: err })); return false; } } @@ -62,7 +65,7 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI return response.ok; } catch (err) { - logger.error(`Failed to fetch from '${this.url("/")}': ${err as string}`); + logger.error(new ErrorWithMetadata("Failed to toggle entity", { entityId }, { cause: err })); return false; } } diff --git a/packages/integrations/src/linuxserverio/linuxserverio-integration.ts b/packages/integrations/src/linuxserverio/linuxserverio-integration.ts index 8b5d1a69b..ebe3aa4a5 100644 --- a/packages/integrations/src/linuxserverio/linuxserverio-integration.ts +++ b/packages/integrations/src/linuxserverio/linuxserverio-integration.ts @@ -1,5 +1,5 @@ import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; @@ -9,7 +9,7 @@ import type { ReleasesProviderIntegration } from "../interfaces/releases-provide import type { ReleaseResponse } from "../interfaces/releases-providers/releases-providers-types"; import { releasesResponseSchema } from "./linuxserverio-schemas"; -const localLogger = logger.child({ module: "LinuxServerIOsIntegration" }); +const logger = createLogger({ module: "linuxServerIOIntegration" }); export class LinuxServerIOIntegration extends Integration implements ReleasesProviderIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { @@ -27,10 +27,7 @@ export class LinuxServerIOIntegration extends Integration implements ReleasesPro private parseIdentifier(identifier: string) { const [owner, name] = identifier.split("/"); if (!owner || !name) { - localLogger.warn( - `Invalid identifier format. Expected 'owner/name', for ${identifier} with LinuxServerIO integration`, - { identifier }, - ); + logger.warn("Invalid identifier format. Expected 'owner/name' for identifier", { identifier }); return null; } return { owner, name }; @@ -53,7 +50,7 @@ export class LinuxServerIOIntegration extends Integration implements ReleasesPro const release = data.data.repositories.linuxserver.find((repo) => repo.name === name); if (!release) { - localLogger.warn(`Repository ${name} not found on provider, with LinuxServerIO integration`, { + logger.warn("Repository not found on provider", { name, }); return { success: false, error: { code: "noMatchingVersion" } }; diff --git a/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts b/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts index f047048b1..d8ca04b15 100644 --- a/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts +++ b/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts @@ -1,7 +1,7 @@ import { z } from "zod/v4"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; @@ -11,6 +11,8 @@ import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-in import type { CalendarEvent, CalendarLink } from "../../interfaces/calendar/calendar-types"; import { mediaOrganizerPriorities } from "../media-organizer"; +const logger = createLogger({ module: "lidarrIntegration" }); + export class LidarrIntegration extends Integration implements ICalendarIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { const response = await input.fetchAsync(this.url("/api"), { diff --git a/packages/integrations/src/media-organizer/radarr/radarr-integration.ts b/packages/integrations/src/media-organizer/radarr/radarr-integration.ts index ce89df608..ee42dbebb 100644 --- a/packages/integrations/src/media-organizer/radarr/radarr-integration.ts +++ b/packages/integrations/src/media-organizer/radarr/radarr-integration.ts @@ -1,7 +1,7 @@ import { z } from "zod/v4"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationTestingInput } from "../../base/integration"; import { Integration } from "../../base/integration"; @@ -12,6 +12,8 @@ import type { CalendarEvent, CalendarLink } from "../../interfaces/calendar/cale import { radarrReleaseTypes } from "../../interfaces/calendar/calendar-types"; import { mediaOrganizerPriorities } from "../media-organizer"; +const logger = createLogger({ module: "radarrIntegration" }); + export class RadarrIntegration extends Integration implements ICalendarIntegration { /** * Gets the events in the Radarr calendar between two dates. diff --git a/packages/integrations/src/media-organizer/readarr/readarr-integration.ts b/packages/integrations/src/media-organizer/readarr/readarr-integration.ts index f7b460d9f..1390fb560 100644 --- a/packages/integrations/src/media-organizer/readarr/readarr-integration.ts +++ b/packages/integrations/src/media-organizer/readarr/readarr-integration.ts @@ -1,7 +1,7 @@ import { z } from "zod/v4"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; @@ -11,6 +11,8 @@ import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-in import type { CalendarEvent, CalendarLink } from "../../interfaces/calendar/calendar-types"; import { mediaOrganizerPriorities } from "../media-organizer"; +const logger = createLogger({ module: "readarrIntegration" }); + export class ReadarrIntegration extends Integration implements ICalendarIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { const response = await input.fetchAsync(this.url("/api"), { diff --git a/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts b/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts index 71a739a37..d35b54365 100644 --- a/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts +++ b/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts @@ -1,7 +1,7 @@ import { z } from "zod/v4"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; @@ -11,6 +11,8 @@ import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-in import type { CalendarEvent, CalendarLink } from "../../interfaces/calendar/calendar-types"; import { mediaOrganizerPriorities } from "../media-organizer"; +const logger = createLogger({ module: "sonarrIntegration" }); + export class SonarrIntegration extends Integration implements ICalendarIntegration { /** * Gets the events in the Sonarr calendar between two dates. diff --git a/packages/integrations/src/nextcloud/nextcloud.integration.ts b/packages/integrations/src/nextcloud/nextcloud.integration.ts index f767fb671..640c86273 100644 --- a/packages/integrations/src/nextcloud/nextcloud.integration.ts +++ b/packages/integrations/src/nextcloud/nextcloud.integration.ts @@ -7,7 +7,7 @@ import * as ical from "node-ical"; import { DAVClient } from "tsdav"; import { createHttpsAgentAsync } from "@homarr/certificates/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { HandleIntegrationErrors } from "../base/errors/decorator"; import { integrationTsdavHttpErrorHandler } from "../base/errors/http"; @@ -17,6 +17,8 @@ import type { TestingResult } from "../base/test-connection/test-connection-serv import type { ICalendarIntegration } from "../interfaces/calendar/calendar-integration"; import type { CalendarEvent } from "../interfaces/calendar/calendar-types"; +const logger = createLogger({ module: "nextcloudIntegration" }); + dayjs.extend(utc); dayjs.extend(timezone); diff --git a/packages/integrations/src/openmediavault/openmediavault-integration.ts b/packages/integrations/src/openmediavault/openmediavault-integration.ts index 0e5ad1171..ee45a996d 100644 --- a/packages/integrations/src/openmediavault/openmediavault-integration.ts +++ b/packages/integrations/src/openmediavault/openmediavault-integration.ts @@ -2,7 +2,7 @@ import type { Headers, HeadersInit, fetch as undiciFetch, Response as UndiciResp import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationInput, IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; @@ -13,7 +13,7 @@ import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-mo import type { SystemHealthMonitoring } from "../types"; import { cpuTempSchema, fileSystemSchema, smartSchema, systemInformationSchema } from "./openmediavault-types"; -const localLogger = logger.child({ module: "OpenMediaVaultIntegration" }); +const logger = createLogger({ module: "openMediaVaultIntegration" }); type SessionStoreValue = | { type: "header"; sessionId: string } @@ -151,13 +151,13 @@ export class OpenMediaVaultIntegration extends Integration implements ISystemHea const storedSession = await this.sessionStore.getAsync(); if (storedSession) { - localLogger.debug("Using stored session for request", { integrationId: this.integration.id }); + logger.debug("Using stored session for request", { integrationId: this.integration.id }); const response = await callback(storedSession); if (response.status !== 401) { return response; } - localLogger.debug("Session expired, getting new session", { integrationId: this.integration.id }); + logger.debug("Session expired, getting new session", { integrationId: this.integration.id }); } const session = await this.getSessionAsync(); diff --git a/packages/integrations/src/overseerr/overseerr-integration.ts b/packages/integrations/src/overseerr/overseerr-integration.ts index 0127b2be6..88846ff49 100644 --- a/packages/integrations/src/overseerr/overseerr-integration.ts +++ b/packages/integrations/src/overseerr/overseerr-integration.ts @@ -1,7 +1,8 @@ import { z } from "zod/v4"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; @@ -21,6 +22,8 @@ import { UpstreamMediaRequestStatus, } from "../interfaces/media-requests/media-request-types"; +const logger = createLogger({ module: "overseerrIntegration" }); + interface OverseerrSearchResult { id: number; name: string; @@ -236,7 +239,7 @@ export class OverseerrIntegration } public async approveRequestAsync(requestId: number): Promise { - logger.info(`Approving media request id='${requestId}' integration='${this.integration.name}'`); + logger.info("Approving media request", { requestId, integration: this.integration.name }); await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/request/${requestId}/approve`), { method: "POST", headers: { @@ -245,16 +248,22 @@ export class OverseerrIntegration }).then((response) => { if (!response.ok) { logger.error( - `Failed to approve media request id='${requestId}' integration='${this.integration.name}' reason='${response.status} ${response.statusText}' url='${response.url}'`, + new ErrorWithMetadata("Failed to approve media request", { + requestId, + integration: this.integration.name, + reason: `${response.status} ${response.statusText}`, + url: response.url, + }), ); } - logger.info(`Successfully approved media request id='${requestId}' integration='${this.integration.name}'`); + logger.info("Successfully approved media request", { requestId, integration: this.integration.name }); }); } public async declineRequestAsync(requestId: number): Promise { - logger.info(`Declining media request id='${requestId}' integration='${this.integration.name}'`); + logger.info("Declining media request", { requestId, integration: this.integration.name }); + await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/request/${requestId}/decline`), { method: "POST", headers: { @@ -263,11 +272,16 @@ export class OverseerrIntegration }).then((response) => { if (!response.ok) { logger.error( - `Failed to decline media request id='${requestId}' integration='${this.integration.name}' reason='${response.status} ${response.statusText}' url='${response.url}'`, + new ErrorWithMetadata("Failed to decline media request", { + requestId, + integration: this.integration.name, + reason: `${response.status} ${response.statusText}`, + url: response.url, + }), ); } - logger.info(`Successfully declined media request id='${requestId}' integration='${this.integration.name}'`); + logger.info("Successfully declined media request", { requestId, integration: this.integration.name }); }); } diff --git a/packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts b/packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts index cf9b63599..b4a472e16 100644 --- a/packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts +++ b/packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts @@ -3,7 +3,7 @@ import type { z } from "zod/v4"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationInput, IntegrationTestingInput } from "../../base/integration"; import { Integration } from "../../base/integration"; @@ -14,7 +14,7 @@ import type { DnsHoleSummaryIntegration } from "../../interfaces/dns-hole-summar import type { DnsHoleSummary } from "../../types"; import { dnsBlockingGetSchema, sessionResponseSchema, statsSummaryGetSchema } from "./pi-hole-schemas-v6"; -const localLogger = logger.child({ module: "PiHoleIntegrationV6" }); +const logger = createLogger({ module: "piHoleIntegration", version: "v6" }); export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIntegration { private readonly sessionStore: SessionStore<{ sid: string | null }>; @@ -126,13 +126,13 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn const storedSession = await this.sessionStore.getAsync(); if (storedSession) { - localLogger.debug("Using stored session for request", { integrationId: this.integration.id }); + logger.debug("Using stored session for request", { integrationId: this.integration.id }); const response = await callback(storedSession.sid); if (response.status !== 401) { return response; } - localLogger.debug("Session expired, getting new session", { integrationId: this.integration.id }); + logger.debug("Session expired, getting new session", { integrationId: this.integration.id }); } const sessionId = await this.getSessionAsync(); @@ -171,7 +171,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn ); } - localLogger.info("Received session id successfully", { integrationId: this.integration.id }); + logger.info("Received session id successfully", { integrationId: this.integration.id }); return result.session.sid; } @@ -185,7 +185,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn fetchAsync: typeof undiciFetch = fetchWithTrustedCertificatesAsync, ) { if (!sessionId) { - localLogger.debug("No session id to clear"); + logger.debug("No session id to clear"); return; } @@ -197,7 +197,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn }); if (!response.ok) { - localLogger.warn("Failed to clear session", { statusCode: response.status, content: await response.text() }); + logger.warn("Failed to clear session", { statusCode: response.status, content: await response.text() }); } logger.debug("Cleared session successfully"); diff --git a/packages/integrations/src/plex/plex-integration.ts b/packages/integrations/src/plex/plex-integration.ts index ee5b9a6da..f8ca3367b 100644 --- a/packages/integrations/src/plex/plex-integration.ts +++ b/packages/integrations/src/plex/plex-integration.ts @@ -3,8 +3,8 @@ import { z } from "zod/v4"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ParseError } from "@homarr/common/server"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { ImageProxy } from "@homarr/image-proxy"; -import { logger } from "@homarr/log"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; @@ -15,6 +15,8 @@ import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-se import type { IMediaReleasesIntegration, MediaRelease } from "../types"; import type { PlexResponse } from "./interface"; +const logger = createLogger({ module: "plexIntegration" }); + export class PlexIntegration extends Integration implements IMediaServerIntegration, IMediaReleasesIntegration { public async getCurrentSessionsAsync(_options: CurrentSessionsInput): Promise { const token = super.getSecretValue("apiKey"); diff --git a/packages/integrations/src/proxmox/proxmox-integration.ts b/packages/integrations/src/proxmox/proxmox-integration.ts index 6ae65feab..164ca377e 100644 --- a/packages/integrations/src/proxmox/proxmox-integration.ts +++ b/packages/integrations/src/proxmox/proxmox-integration.ts @@ -2,7 +2,7 @@ import type { Proxmox } from "proxmox-api"; import proxmoxApi from "proxmox-api"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { HandleIntegrationErrors } from "../base/errors/decorator"; import type { IntegrationTestingInput } from "../base/integration"; @@ -19,6 +19,8 @@ import type { StorageResource, } from "./proxmox-types"; +const logger = createLogger({ module: "proxmoxIntegration" }); + @HandleIntegrationErrors([new ProxmoxApiErrorHandler()]) export class ProxmoxIntegration extends Integration implements IClusterHealthMonitoringIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { @@ -31,9 +33,13 @@ export class ProxmoxIntegration extends Integration implements IClusterHealthMon const proxmox = this.getPromoxApi(); const resources = await proxmox.cluster.resources.$get(); - logger.info( - `Found ${resources.length} resources in Proxmox cluster node=${resources.filter((resource) => resource.type === "node").length} lxc=${resources.filter((resource) => resource.type === "lxc").length} qemu=${resources.filter((resource) => resource.type === "qemu").length} storage=${resources.filter((resource) => resource.type === "storage").length}`, - ); + logger.info("Found resources in Proxmox cluster", { + total: resources.length, + node: resources.filter((resource) => resource.type === "node").length, + lxc: resources.filter((resource) => resource.type === "lxc").length, + qemu: resources.filter((resource) => resource.type === "qemu").length, + storage: resources.filter((resource) => resource.type === "storage").length, + }); const mappedResources = resources.map(mapResource).filter((resource) => resource !== null); return { diff --git a/packages/integrations/src/quay/quay-integration.ts b/packages/integrations/src/quay/quay-integration.ts index f27349e6b..ac150e53a 100644 --- a/packages/integrations/src/quay/quay-integration.ts +++ b/packages/integrations/src/quay/quay-integration.ts @@ -1,7 +1,7 @@ import type { RequestInit, Response } from "undici"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; @@ -15,7 +15,7 @@ import type { } from "../interfaces/releases-providers/releases-providers-types"; import { releasesResponseSchema } from "./quay-schemas"; -const localLogger = logger.child({ module: "QuayIntegration" }); +const logger = createLogger({ module: "quayIntegration" }); export class QuayIntegration extends Integration implements ReleasesProviderIntegration { private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise): Promise { @@ -45,7 +45,7 @@ export class QuayIntegration extends Integration implements ReleasesProviderInte private parseIdentifier(identifier: string) { const [owner, name] = identifier.split("/"); if (!owner || !name) { - localLogger.warn(`Invalid identifier format. Expected 'owner/name', for ${identifier} with Quay integration`, { + logger.warn("Invalid identifier format. Expected 'owner/name' for identifier", { identifier, }); return null; diff --git a/packages/integrations/src/truenas/truenas-integration.ts b/packages/integrations/src/truenas/truenas-integration.ts index c31b873ba..206f033a0 100644 --- a/packages/integrations/src/truenas/truenas-integration.ts +++ b/packages/integrations/src/truenas/truenas-integration.ts @@ -3,7 +3,7 @@ import z from "zod"; import { createId } from "@homarr/common"; import { RequestError, ResponseError } from "@homarr/common/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; @@ -11,7 +11,7 @@ import type { TestingResult } from "../base/test-connection/test-connection-serv import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-monitoring/health-monitoring-integration"; import type { SystemHealthMonitoring } from "../interfaces/health-monitoring/health-monitoring-types"; -const localLogger = logger.child({ module: "TrueNasIntegration" }); +const logger = createLogger({ module: "trueNasIntegration" }); const NETWORK_MULTIPLIER = 100; @@ -45,14 +45,14 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni * @see https://www.truenas.com/docs/api/scale_websocket_api.html */ private async connectWebSocketAsync(): Promise { - localLogger.debug("Connecting to websocket server", { + logger.debug("Connecting to websocket server", { url: this.wsUrl(), }); const webSocket = new WebSocket(this.wsUrl()); return new Promise((resolve, reject) => { webSocket.onopen = () => { - localLogger.debug("Connected to websocket server", { + logger.debug("Connected to websocket server", { url: this.wsUrl(), }); resolve(webSocket); @@ -97,7 +97,7 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni * @see https://www.truenas.com/docs/api/scale_websocket_api.html#websocket_protocol */ private async authenticateWebSocketAsync(webSocket?: WebSocket): Promise { - localLogger.debug("Authenticating with username and password", { + logger.debug("Authenticating with username and password", { url: this.wsUrl(), }); const response = await this.requestAsync( @@ -107,7 +107,7 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni ); const result = await z.boolean().parseAsync(response); if (!result) throw new ResponseError({ status: 401 }); - localLogger.debug("Authenticated successfully with username and password", { + logger.debug("Authenticated successfully with username and password", { url: this.wsUrl(), }); } @@ -117,7 +117,7 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni * @see https://www.truenas.com/docs/api/scale_websocket_api.html#reporting */ private async getReportingAsync(): Promise { - localLogger.debug("Retrieving reporting data", { + logger.debug("Retrieving reporting data", { url: this.wsUrl(), }); @@ -141,7 +141,7 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni ]); const result = await z.array(reportingItemSchema).parseAsync(response); - localLogger.debug("Retrieved reporting data", { + logger.debug("Retrieved reporting data", { url: this.wsUrl(), count: result.length, }); @@ -153,7 +153,7 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni * @see https://www.truenas.com/docs/core/13.0/api/core_websocket_api.html#interface */ private async getNetworkInterfacesAsync(): Promise> { - localLogger.debug("Retrieving available network-interfaces", { + logger.debug("Retrieving available network-interfaces", { url: this.wsUrl(), }); @@ -163,7 +163,7 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni ]); const result = await networkInterfaceSchema.parseAsync(response); - localLogger.debug("Retrieved available network-interfaces", { + logger.debug("Retrieved available network-interfaces", { url: this.wsUrl(), count: result.length, }); @@ -177,7 +177,7 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni private async getReportingNetdataAsync(): Promise> { const networkInterfaces = await this.getNetworkInterfacesAsync(); - localLogger.debug("Retrieving reporting network data", { + logger.debug("Retrieving reporting network data", { url: this.wsUrl(), }); @@ -193,7 +193,7 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni ]); const result = await reportingNetDataSchema.parseAsync(response); - localLogger.debug("Retrieved reporting-network-data", { + logger.debug("Retrieved reporting-network-data", { url: this.wsUrl(), count: result.length, }); @@ -205,14 +205,14 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni * @see https://www.truenas.com/docs/api/scale_websocket_api.html#system */ private async getSystemInformationAsync(): Promise> { - localLogger.debug("Retrieving system-information", { + logger.debug("Retrieving system-information", { url: this.wsUrl(), }); const response = await this.requestAsync("system.info"); const result = await systemInfoSchema.parseAsync(response); - localLogger.debug("Retrieved system-information", { + logger.debug("Retrieved system-information", { url: this.wsUrl(), }); return result; @@ -262,7 +262,7 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni private async requestAsync(method: string, params: unknown[] = [], webSocketOverride?: WebSocket) { let webSocket = webSocketOverride ?? this.webSocket; if (!webSocket || webSocket.readyState !== WebSocket.OPEN) { - localLogger.debug("Connecting to websocket", { + logger.debug("Connecting to websocket", { url: this.wsUrl(), }); // We can only land here with static webSocket @@ -282,7 +282,7 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni clearTimeout(timeoutId); webSocket.removeEventListener("message", handler); - localLogger.debug("Received method response", { + logger.debug("Received method response", { id, method, url: this.wsUrl(), @@ -305,7 +305,7 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni webSocket.addEventListener("message", handler); - localLogger.debug("Sending method request", { + logger.debug("Sending method request", { id, method, url: this.wsUrl(), diff --git a/packages/log/eslint.config.js b/packages/log/eslint.config.js deleted file mode 100644 index f7a5a7d36..000000000 --- a/packages/log/eslint.config.js +++ /dev/null @@ -1,4 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; - -/** @type {import('typescript-eslint').Config} */ -export default [...baseConfig]; diff --git a/packages/log/package.json b/packages/log/package.json deleted file mode 100644 index 8cc811cac..000000000 --- a/packages/log/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@homarr/log", - "version": "0.1.0", - "private": true, - "license": "Apache-2.0", - "type": "module", - "exports": { - ".": "./src/index.ts", - "./constants": "./src/constants.ts", - "./env": "./src/env.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": { - "@homarr/core": "workspace:^0.1.0", - "superjson": "2.2.6", - "winston": "3.19.0", - "zod": "^4.1.13" - }, - "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", - "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.39.1", - "typescript": "^5.9.3" - } -} diff --git a/packages/log/src/env.ts b/packages/log/src/env.ts deleted file mode 100644 index 28d463bd6..000000000 --- a/packages/log/src/env.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from "zod/v4"; - -import { createEnv } from "@homarr/core/infrastructure/env"; - -import { logLevels } from "./constants"; - -export const env = createEnv({ - server: { - LOG_LEVEL: z.enum(logLevels).default("info"), - }, - experimental__runtimeEnv: process.env, -}); diff --git a/packages/log/src/index.ts b/packages/log/src/index.ts deleted file mode 100644 index 4337ffb53..000000000 --- a/packages/log/src/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { transport as Transport } from "winston"; -import winston, { format, transports } from "winston"; - -import { env } from "./env"; -import { formatErrorCause, formatErrorStack } from "./error"; -import { formatMetadata } from "./metadata"; -import { RedisTransport } from "./redis-transport"; - -const logMessageFormat = format.printf(({ level, message, timestamp, cause, stack, ...metadata }) => { - if (!cause && !stack) { - return `${timestamp as string} ${level}: ${message as string}`; - } - - const formatedStack = formatErrorStack(stack as string | undefined); - - if (!cause) { - return `${timestamp as string} ${level}: ${message as string} ${formatMetadata(metadata)}\n${formatedStack}`; - } - - return `${timestamp as string} ${level}: ${message as string} ${formatMetadata(metadata)}\n${formatedStack}${formatErrorCause(cause)}`; -}); - -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))) { - logTransports.push( - new RedisTransport({ - level: "debug", - }), - ); -} - -const logger = winston.createLogger({ - format: format.combine( - format.colorize(), - format.timestamp(), - format.errors({ stack: true, cause: true }), - logMessageFormat, - ), - transports: logTransports, - level: env.LOG_LEVEL, -}); - -export { logger }; diff --git a/packages/log/tsconfig.json b/packages/log/tsconfig.json deleted file mode 100644 index cbe8483d9..000000000 --- a/packages/log/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "@homarr/tsconfig/base.json", - "compilerOptions": { - "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" - }, - "include": ["*.ts", "src"], - "exclude": ["node_modules"] -} diff --git a/packages/old-import/package.json b/packages/old-import/package.json index 12335d64e..e252d38fd 100644 --- a/packages/old-import/package.json +++ b/packages/old-import/package.json @@ -27,10 +27,10 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^0.1.0", + "@homarr/core": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", "@homarr/form": "workspace:^0.1.0", - "@homarr/log": "workspace:^0.1.0", "@homarr/modals": "workspace:^0.1.0", "@homarr/notifications": "workspace:^0.1.0", "@homarr/old-schema": "workspace:^0.1.0", diff --git a/packages/old-import/src/analyse/analyse-oldmarr-import.ts b/packages/old-import/src/analyse/analyse-oldmarr-import.ts index c526728af..2a440b979 100644 --- a/packages/old-import/src/analyse/analyse-oldmarr-import.ts +++ b/packages/old-import/src/analyse/analyse-oldmarr-import.ts @@ -1,12 +1,15 @@ import AdmZip from "adm-zip"; import { z } from "zod/v4"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { oldmarrConfigSchema } from "@homarr/old-schema"; import { oldmarrImportUserSchema } from "../user-schema"; import type { analyseOldmarrImportInputSchema } from "./input"; +const logger = createLogger({ module: "analyseOldmarrImport" }); + export const analyseOldmarrImportForRouterAsync = async (input: z.infer) => { const { configs, checksum, users } = await analyseOldmarrImportAsync(input.file); @@ -25,7 +28,13 @@ export const analyseOldmarrImportAsync = async (file: File) => { const configs = configEntries.map((entry) => { const result = oldmarrConfigSchema.safeParse(JSON.parse(entry.getData().toString())); if (!result.success) { - logger.error(`Failed to parse config ${entry.entryName} with error: ${JSON.stringify(result.error)}`); + logger.error( + new ErrorWithMetadata( + "Failed to parse oldmarr config", + { entryName: entry.entryName }, + { cause: result.error }, + ), + ); } return { @@ -57,7 +66,7 @@ const parseUsers = (entry: AdmZip.IZipEntry | undefined) => { const result = z.array(oldmarrImportUserSchema).safeParse(JSON.parse(entry.getData().toString())); if (!result.success) { - logger.error(`Failed to parse users with error: ${JSON.stringify(result.error)}`); + logger.error(new Error("Failed to parse users", { cause: result.error })); } return result.data ?? []; diff --git a/packages/old-import/src/fix-section-issues.ts b/packages/old-import/src/fix-section-issues.ts index b34c24871..0a092deec 100644 --- a/packages/old-import/src/fix-section-issues.ts +++ b/packages/old-import/src/fix-section-issues.ts @@ -1,7 +1,9 @@ import { createId } from "@homarr/common"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { OldmarrConfig } from "@homarr/old-schema"; +const logger = createLogger({ module: "fixSectionIssues" }); + export const fixSectionIssues = (old: OldmarrConfig) => { const wrappers = old.wrappers.sort((wrapperA, wrapperB) => wrapperA.position - wrapperB.position); const categories = old.categories.sort((categoryA, categoryB) => categoryA.position - categoryB.position); @@ -9,9 +11,10 @@ export const fixSectionIssues = (old: OldmarrConfig) => { const neededSectionCount = categories.length * 2 + 1; const hasToMuchEmptyWrappers = wrappers.length > categories.length + 1; - logger.debug( - `Fixing section issues neededSectionCount=${neededSectionCount} hasToMuchEmptyWrappers=${hasToMuchEmptyWrappers}`, - ); + logger.debug("Fixing section issues", { + neededSectionCount, + hasToMuchEmptyWrappers, + }); for (let position = 0; position < neededSectionCount; position++) { const index = Math.floor(position / 2); @@ -38,7 +41,7 @@ export const fixSectionIssues = (old: OldmarrConfig) => { wrappers.splice(categories.length + 1); if (wrapperIdsToMerge.length >= 2) { - logger.debug(`Found wrappers to merge count=${wrapperIdsToMerge.length}`); + logger.debug("Found wrappers to merge", { count: wrapperIdsToMerge.length }); } return { diff --git a/packages/old-import/src/import-sections.ts b/packages/old-import/src/import-sections.ts index f1900790b..78a1b1619 100644 --- a/packages/old-import/src/import-sections.ts +++ b/packages/old-import/src/import-sections.ts @@ -1,18 +1,18 @@ import { createId } from "@homarr/common"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { Database } from "@homarr/db"; import { sections } from "@homarr/db/schema"; -import { logger } from "@homarr/log"; import type { OldmarrConfig } from "@homarr/old-schema"; +const logger = createLogger({ module: "importSections" }); + export const insertSectionsAsync = async ( db: Database, categories: OldmarrConfig["categories"], wrappers: OldmarrConfig["wrappers"], boardId: string, ) => { - logger.info( - `Importing old homarr sections boardId=${boardId} categories=${categories.length} wrappers=${wrappers.length}`, - ); + logger.info("Importing old homarr sections", { boardId, categories: categories.length, wrappers: wrappers.length }); const wrapperIds = wrappers.map((section) => section.id); const categoryIds = categories.map((section) => section.id); @@ -45,7 +45,7 @@ export const insertSectionsAsync = async ( await db.insert(sections).values(categoriesToInsert); } - logger.info(`Imported sections count=${wrappersToInsert.length + categoriesToInsert.length}`); + logger.info("Imported sections", { count: wrappersToInsert.length + categoriesToInsert.length }); return idMaps; }; diff --git a/packages/old-import/src/import/collections/board-collection.ts b/packages/old-import/src/import/collections/board-collection.ts index df8d72648..65bfb37f0 100644 --- a/packages/old-import/src/import/collections/board-collection.ts +++ b/packages/old-import/src/import/collections/board-collection.ts @@ -1,6 +1,6 @@ import { createId } from "@homarr/common"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { createDbInsertCollectionForTransaction } from "@homarr/db/collection"; -import { logger } from "@homarr/log"; import type { BoardSize, OldmarrConfig } from "@homarr/old-schema"; import { boardSizes, getBoardSizeName } from "@homarr/old-schema"; @@ -15,6 +15,8 @@ import type { prepareMultipleImports } from "../../prepare/prepare-multiple"; import { prepareSections } from "../../prepare/prepare-sections"; import type { InitialOldmarrImportSettings } from "../../settings"; +const logger = createLogger({ module: "boardCollection" }); + export const createBoardInsertCollection = ( { preparedApps, preparedBoards }: Omit, "preparedIntegrations">, settings: InitialOldmarrImportSettings, @@ -50,12 +52,12 @@ export const createBoardInsertCollection = ( } if (settings.onlyImportApps) { - logger.info( - `Skipping boards and sections import due to onlyImportApps setting appCount=${insertCollection.apps.length}`, - ); + logger.info("Skipping boards and sections import due to onlyImportApps setting", { + appCount: insertCollection.apps.length, + }); return insertCollection; } - logger.debug(`Added apps to board insert collection count=${insertCollection.apps.length}`); + logger.debug("Added apps to board insert collection", { count: insertCollection.apps.length }); preparedBoards.forEach((board) => { if (!hasEnoughItemShapes(board.config)) { @@ -71,10 +73,10 @@ export const createBoardInsertCollection = ( name: board.name, }); - logger.debug(`Fixed issues with sections and item positions fileName=${board.name}`); + logger.debug("Fixed issues with sections and item positions", { fileName: board.name }); const mappedBoard = mapBoard(board); - logger.debug(`Mapped board fileName=${board.name} boardId=${mappedBoard.id}`); + logger.debug("Mapped board", { fileName: board.name, boardId: mappedBoard.id }); insertCollection.boards.push(mappedBoard); const layoutMapping = boardSizes.reduce( @@ -100,7 +102,7 @@ export const createBoardInsertCollection = ( for (const section of preparedSections.values()) { insertCollection.sections.push(section); } - logger.debug(`Added sections to board insert collection count=${insertCollection.sections.length}`); + logger.debug("Added sections to board insert collection", { count: insertCollection.sections.length }); const preparedItems = prepareItems( { @@ -117,12 +119,15 @@ export const createBoardInsertCollection = ( insertCollection.items.push(item); insertCollection.itemLayouts.push(...layouts); }); - logger.debug(`Added items to board insert collection count=${insertCollection.items.length}`); + logger.debug("Added items to board insert collection", { count: insertCollection.items.length }); }); - logger.info( - `Board collection prepared boardCount=${insertCollection.boards.length} sectionCount=${insertCollection.sections.length} itemCount=${insertCollection.items.length} appCount=${insertCollection.apps.length}`, - ); + logger.info("Board collection prepared", { + boardCount: insertCollection.boards.length, + sectionCount: insertCollection.sections.length, + itemCount: insertCollection.items.length, + appCount: insertCollection.apps.length, + }); return insertCollection; }; diff --git a/packages/old-import/src/import/collections/integration-collection.ts b/packages/old-import/src/import/collections/integration-collection.ts index 4a891a808..33b182040 100644 --- a/packages/old-import/src/import/collections/integration-collection.ts +++ b/packages/old-import/src/import/collections/integration-collection.ts @@ -1,10 +1,12 @@ import { encryptSecret } from "@homarr/common/server"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { createDbInsertCollectionForTransaction } from "@homarr/db/collection"; -import { logger } from "@homarr/log"; import { mapAndDecryptIntegrations } from "../../mappers/map-integration"; import type { PreparedIntegration } from "../../prepare/prepare-integrations"; +const logger = createLogger({ module: "integrationCollection" }); + export const createIntegrationInsertCollection = ( preparedIntegrations: PreparedIntegration[], encryptionToken: string | null | undefined, @@ -15,7 +17,7 @@ export const createIntegrationInsertCollection = ( return insertCollection; } - logger.info(`Preparing integrations for insert collection count=${preparedIntegrations.length}`); + logger.info("Preparing integrations for insert collection", { count: preparedIntegrations.length }); if (encryptionToken === null || encryptionToken === undefined) { logger.debug("Skipping integration decryption due to missing token"); @@ -44,9 +46,10 @@ export const createIntegrationInsertCollection = ( }); }); - logger.info( - `Added integrations and secrets to insert collection integrationCount=${insertCollection.integrations.length} secretCount=${insertCollection.integrationSecrets.length}`, - ); + logger.info("Added integrations and secrets to insert collection", { + integrationCount: insertCollection.integrations.length, + secretCount: insertCollection.integrationSecrets.length, + }); return insertCollection; }; diff --git a/packages/old-import/src/import/collections/user-collection.ts b/packages/old-import/src/import/collections/user-collection.ts index 5f8a54117..3fb9cc14b 100644 --- a/packages/old-import/src/import/collections/user-collection.ts +++ b/packages/old-import/src/import/collections/user-collection.ts @@ -1,11 +1,13 @@ import { createId } from "@homarr/common"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { createDbInsertCollectionForTransaction } from "@homarr/db/collection"; import { credentialsAdminGroup } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { mapAndDecryptUsers } from "../../mappers/map-user"; import type { OldmarrImportUser } from "../../user-schema"; +const logger = createLogger({ module: "userCollection" }); + export const createUserInsertCollection = ( importUsers: OldmarrImportUser[], encryptionToken: string | null | undefined, @@ -21,7 +23,7 @@ export const createUserInsertCollection = ( return insertCollection; } - logger.info(`Preparing users for insert collection count=${importUsers.length}`); + logger.info("Preparing users for insert collection", { count: importUsers.length }); if (encryptionToken === null || encryptionToken === undefined) { logger.debug("Skipping user decryption due to missing token"); @@ -30,7 +32,7 @@ export const createUserInsertCollection = ( const preparedUsers = mapAndDecryptUsers(importUsers, encryptionToken); preparedUsers.forEach((user) => insertCollection.users.push(user)); - logger.debug(`Added users to insert collection count=${insertCollection.users.length}`); + logger.debug("Added users to insert collection", { count: insertCollection.users.length }); if (!preparedUsers.some((user) => user.isAdmin)) { logger.warn("No admin users found, skipping admin group creation"); @@ -58,9 +60,10 @@ export const createUserInsertCollection = ( }); }); - logger.info( - `Added admin group and permissions to insert collection adminGroupId=${adminGroupId} adminUsersCount=${admins.length}`, - ); + logger.info("Added admin group and permissions to insert collection", { + adminGroupId, + adminUsersCount: admins.length, + }); return insertCollection; }; diff --git a/packages/old-import/src/import/import-initial-oldmarr.ts b/packages/old-import/src/import/import-initial-oldmarr.ts index a42bbba83..f773c1b36 100644 --- a/packages/old-import/src/import/import-initial-oldmarr.ts +++ b/packages/old-import/src/import/import-initial-oldmarr.ts @@ -1,9 +1,9 @@ import type { z } from "zod/v4"; import { Stopwatch } from "@homarr/common"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { handleTransactionsAsync } from "@homarr/db"; import type { Database } from "@homarr/db"; -import { logger } from "@homarr/log"; import { analyseOldmarrImportAsync } from "../analyse/analyse-oldmarr-import"; import { prepareMultipleImports } from "../prepare/prepare-multiple"; @@ -13,6 +13,8 @@ import { createUserInsertCollection } from "./collections/user-collection"; import type { importInitialOldmarrInputSchema } from "./input"; import { ensureValidTokenOrThrow } from "./validate-token"; +const logger = createLogger({ module: "importInitialOldmarr" }); + export const importInitialOldmarrAsync = async ( db: Database, input: z.infer, @@ -52,5 +54,5 @@ export const importInitialOldmarrAsync = async ( }, }); - logger.info(`Import successful (in ${stopwatch.getElapsedInHumanWords()})`); + logger.info("Import successful", { duration: stopwatch.getElapsedInHumanWords() }); }; diff --git a/packages/old-import/src/mappers/map-item.ts b/packages/old-import/src/mappers/map-item.ts index f4c471728..2e339a0b2 100644 --- a/packages/old-import/src/mappers/map-item.ts +++ b/packages/old-import/src/mappers/map-item.ts @@ -1,9 +1,9 @@ import SuperJSON from "superjson"; import { createId } from "@homarr/common"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { InferInsertModel } from "@homarr/db"; import type { itemLayouts, items } from "@homarr/db/schema"; -import { logger } from "@homarr/log"; import type { BoardSize, OldmarrApp, OldmarrWidget } from "@homarr/old-schema"; import { boardSizes } from "@homarr/old-schema"; @@ -11,6 +11,8 @@ import type { WidgetComponentProps } from "../../../widgets/src/definition"; import { mapKind } from "../widgets/definitions"; import { mapOptions } from "../widgets/options"; +const logger = createLogger({ module: "mapItem" }); + export const mapApp = ( app: OldmarrApp, appsMap: Map, @@ -22,7 +24,10 @@ export const mapApp = ( const sectionId = sectionMap.get(app.area.properties.id)?.id; if (!sectionId) { - logger.warn(`Failed to find section for app appId='${app.id}' sectionId='${app.area.properties.id}'. Removing app`); + logger.warn("Failed to find section for app. Removing app", { + appId: app.id, + sectionId: app.area.properties.id, + }); return null; } @@ -71,15 +76,19 @@ export const mapWidget = ( const kind = mapKind(widget.type); if (!kind) { - logger.warn(`Failed to map widget type='${widget.type}'. It's no longer supported`); + logger.warn("Failed to map widget type. It's no longer supported", { + widgetId: widget.id, + widgetType: widget.type, + }); return null; } const sectionId = sectionMap.get(widget.area.properties.id)?.id; if (!sectionId) { - logger.warn( - `Failed to find section for widget widgetId='${widget.id}' sectionId='${widget.area.properties.id}'. Removing widget`, - ); + logger.warn("Failed to find section for widget. Removing widget", { + widgetId: widget.id, + sectionId: widget.area.properties.id, + }); return null; } diff --git a/packages/old-import/src/move-widgets-and-apps-merge.ts b/packages/old-import/src/move-widgets-and-apps-merge.ts index 5b77fa925..5cb74a160 100644 --- a/packages/old-import/src/move-widgets-and-apps-merge.ts +++ b/packages/old-import/src/move-widgets-and-apps-merge.ts @@ -1,11 +1,13 @@ import { objectEntries } from "@homarr/common"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { BoardSize, OldmarrApp, OldmarrConfig, OldmarrWidget } from "@homarr/old-schema"; import { boardSizes } from "@homarr/old-schema"; import { mapColumnCount } from "./mappers/map-column-count"; import type { OldmarrImportConfiguration } from "./settings"; +const logger = createLogger({ module: "moveWidgetsAndAppsMerge" }); + export const moveWidgetsAndAppsIfMerge = ( old: OldmarrConfig, wrapperIdsToMerge: string[], @@ -26,7 +28,7 @@ export const moveWidgetsAndAppsIfMerge = ( ]), ); - logger.debug(`Merging wrappers at the end of the board count=${wrapperIdsToMerge.length}`); + logger.debug("Merging wrappers at the end of the board", { count: wrapperIdsToMerge.length }); const offsets = boardSizes.reduce( (previous, screenSize) => { diff --git a/packages/old-import/src/prepare/prepare-items.ts b/packages/old-import/src/prepare/prepare-items.ts index bbb96f171..0f32d6b00 100644 --- a/packages/old-import/src/prepare/prepare-items.ts +++ b/packages/old-import/src/prepare/prepare-items.ts @@ -1,4 +1,4 @@ -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { BoardSize, OldmarrApp, OldmarrConfig, OldmarrWidget, SizedShape } from "@homarr/old-schema"; import { boardSizes } from "@homarr/old-schema"; @@ -7,6 +7,8 @@ import { generateResponsiveGridFor } from "../../../api/src/router/board/grid-al import { mapColumnCount } from "../mappers/map-column-count"; import { mapApp, mapWidget } from "../mappers/map-item"; +const logger = createLogger({ module: "prepareItems" }); + export const prepareItems = ( { apps, widgets, settings }: Pick, appsMap: Map, @@ -25,15 +27,17 @@ export const prepareItems = ( ); if (incompleteSizes.length > 0) { - logger.warn( - `Found items with incomplete sizes board=${boardId} count=${incompleteSizes.length} sizes=${incompleteSizes.join(", ")}\nHomarr will automatically generate missing sizes`, - ); + logger.warn("Found items with incomplete sizes. Generating missing sizes.", { + boardId, + count: incompleteSizes.length, + sizes: incompleteSizes.join(", "), + }); incompleteSizes.forEach((size) => { const columnCount = mapColumnCount(settings.customization.gridstack, size); const previousSize = !incompleteSizes.includes("lg") ? "lg" : incompleteSizes.includes("sm") ? "md" : "sm"; const previousWidth = mapColumnCount(settings.customization.gridstack, previousSize); - logger.info(`Generating missing size boardId=${boardId} from=${previousSize} to=${size}`); + logger.info("Generating missing size", { boardId, from: previousSize, to: size }); const items = widgets .map((item) => mapItemForGridAlgorithm(item, previousSize)) diff --git a/packages/old-import/src/widgets/options.ts b/packages/old-import/src/widgets/options.ts index 60c5302f6..e3c58768f 100644 --- a/packages/old-import/src/widgets/options.ts +++ b/packages/old-import/src/widgets/options.ts @@ -1,10 +1,12 @@ import { objectEntries } from "@homarr/common"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { WidgetComponentProps } from "../../../widgets/src/definition"; import type { InversedWidgetMapping, OldmarrWidgetDefinitions, WidgetMapping } from "./definitions"; import { mapKind } from "./definitions"; +const logger = createLogger({ module: "mapOptions" }); + // This type enforces, that for all widget mappings there is a corresponding option mapping, // each option of newmarr can be mapped from the value of the oldmarr options type OptionMapping = { @@ -192,7 +194,7 @@ export const mapOptions = ( oldOptions: Extract["options"], appsMap: Map, ) => { - logger.debug(`Mapping old homarr options for widget type=${type} options=${JSON.stringify(oldOptions)}`); + logger.debug("Mapping old homarr options for widget", { type, options: JSON.stringify(oldOptions) }); const kind = mapKind(type); if (!kind) { return null; @@ -202,7 +204,7 @@ export const mapOptions = ( return objectEntries(mapping).reduce( (acc, [key, value]: [string, (oldOptions: Record, appsMap: Map) => unknown]) => { const newValue = value(oldOptions, appsMap); - logger.debug(`Mapping old homarr option kind=${kind} key=${key} newValue=${newValue as string}`); + logger.debug("Mapping old homarr option", { kind, key, newValue }); if (newValue !== undefined) { acc[key] = newValue; } diff --git a/packages/ping/package.json b/packages/ping/package.json index 364ca9f86..f6f6a3133 100644 --- a/packages/ping/package.json +++ b/packages/ping/package.json @@ -24,7 +24,7 @@ "dependencies": { "@homarr/certificates": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", - "@homarr/log": "workspace:^0.1.0" + "@homarr/core": "workspace:^0.1.0" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", diff --git a/packages/ping/src/index.ts b/packages/ping/src/index.ts index 1bf624706..9e581beee 100644 --- a/packages/ping/src/index.ts +++ b/packages/ping/src/index.ts @@ -2,7 +2,10 @@ import { fetch } from "undici"; import { extractErrorMessage } from "@homarr/common"; import { LoggingAgent } from "@homarr/common/server"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; + +const logger = createLogger({ module: "ping" }); export const sendPingRequestAsync = async (url: string) => { try { @@ -29,7 +32,7 @@ export const sendPingRequestAsync = async (url: string) => { return { statusCode: response.status, durationMs }; }); } catch (error) { - logger.error(new Error(`Failed to send ping request to "${url}"`, { cause: error })); + logger.error(new ErrorWithMetadata("Failed to send ping request", { url }, { cause: error })); return { error: extractErrorMessage(error), }; diff --git a/packages/redis/package.json b/packages/redis/package.json index 97df1742d..beab8a492 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -26,7 +26,6 @@ "@homarr/core": "workspace:^", "@homarr/db": "workspace:^", "@homarr/definitions": "workspace:^", - "@homarr/log": "workspace:^", "ioredis": "5.8.2", "superjson": "2.2.6" }, diff --git a/packages/redis/src/index.ts b/packages/redis/src/index.ts index bd44350c2..f8d8fd329 100644 --- a/packages/redis/src/index.ts +++ b/packages/redis/src/index.ts @@ -1,4 +1,4 @@ -import type { LogLevel } from "@homarr/log/constants"; +import type { LogLevel } from "@homarr/core/infrastructure/logs/constants"; import { createListChannel, createQueueChannel, createSubPubChannel } from "./lib/channel"; diff --git a/packages/redis/src/lib/channel-subscription-tracker.ts b/packages/redis/src/lib/channel-subscription-tracker.ts index 8b98b4732..02adc6897 100644 --- a/packages/redis/src/lib/channel-subscription-tracker.ts +++ b/packages/redis/src/lib/channel-subscription-tracker.ts @@ -1,10 +1,12 @@ import { randomUUID } from "crypto"; import type { MaybePromise } from "@homarr/common/types"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { createRedisConnection } from "./connection"; +const logger = createLogger({ module: "channelSubscriptionTracker" }); + type SubscriptionCallback = (message: string) => MaybePromise; /** @@ -26,7 +28,7 @@ export class ChannelSubscriptionTracker { * @returns a function to unsubscribe from the channel */ public static subscribe(channelName: string, callback: SubscriptionCallback) { - logger.debug(`Adding redis channel callback channel='${channelName}'`); + logger.debug("Adding redis channel callback", { channel: channelName }); // We only want to activate the listener once if (!this.listenerActive) { @@ -39,18 +41,18 @@ export class ChannelSubscriptionTracker { // If there are no subscriptions to the channel, subscribe to it if (channelSubscriptions.size === 0) { - logger.debug(`Subscribing to redis channel channel='${channelName}'`); + logger.debug("Subscribing to redis channel", { channel: channelName }); void this.redis.subscribe(channelName); } - logger.debug(`Adding redis channel callback channel='${channelName}' id='${id}'`); + logger.debug("Adding redis channel callback", { channel: channelName, id }); channelSubscriptions.set(id, callback); this.subscriptions.set(channelName, channelSubscriptions); // Return a function to unsubscribe return () => { - logger.debug(`Removing redis channel callback channel='${channelName}' id='${id}'`); + logger.debug("Removing redis channel callback", { channel: channelName, id }); const channelSubscriptions = this.subscriptions.get(channelName); if (!channelSubscriptions) return; @@ -62,7 +64,7 @@ export class ChannelSubscriptionTracker { return; } - logger.debug(`Unsubscribing from redis channel channel='${channelName}'`); + logger.debug("Unsubscribing from redis channel", { channel: channelName }); void this.redis.unsubscribe(channelName); this.subscriptions.delete(channelName); }; @@ -76,14 +78,14 @@ export class ChannelSubscriptionTracker { this.redis.on("message", (channel, message) => { const channelSubscriptions = this.subscriptions.get(channel); if (!channelSubscriptions) { - logger.warn(`Received message on unknown channel channel='${channel}'`); + logger.warn("Received message on unknown channel", { channel }); return; } for (const [id, callback] of channelSubscriptions.entries()) { // Don't log messages from the logging channel as it would create an infinite loop if (channel !== "pubSub:logging") { - logger.debug(`Calling subscription callback channel='${channel}' id='${id}'`); + logger.debug("Calling subscription callback", { channel, id }); } void callback(message); } diff --git a/packages/redis/src/lib/channel.ts b/packages/redis/src/lib/channel.ts index 074f66414..193f551e3 100644 --- a/packages/redis/src/lib/channel.ts +++ b/packages/redis/src/lib/channel.ts @@ -1,12 +1,14 @@ import superjson from "superjson"; import { createId, hashObjectBase64 } from "@homarr/common"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { WidgetKind } from "@homarr/definitions"; -import { logger } from "@homarr/log"; import { ChannelSubscriptionTracker } from "./channel-subscription-tracker"; import { createRedisConnection } from "./connection"; +const logger = createLogger({ module: "redisChannel" }); + const publisher = createRedisConnection(); const lastDataClient = createRedisConnection(); diff --git a/packages/request-handler/package.json b/packages/request-handler/package.json index 0c3a0af88..ffb83af24 100644 --- a/packages/request-handler/package.json +++ b/packages/request-handler/package.json @@ -24,11 +24,11 @@ "dependencies": { "@extractus/feed-extractor": "7.1.7", "@homarr/common": "workspace:^0.1.0", + "@homarr/core": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", "@homarr/docker": "workspace:^0.1.0", "@homarr/integrations": "workspace:^0.1.0", - "@homarr/log": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", "dayjs": "^1.11.19", "octokit": "^5.0.5", diff --git a/packages/request-handler/src/lib/cached-request-handler.ts b/packages/request-handler/src/lib/cached-request-handler.ts index 2e1739248..d9b8dfa90 100644 --- a/packages/request-handler/src/lib/cached-request-handler.ts +++ b/packages/request-handler/src/lib/cached-request-handler.ts @@ -1,9 +1,11 @@ import dayjs from "dayjs"; import type { Duration } from "dayjs/plugin/duration"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import type { createChannelWithLatestAndEvents } from "@homarr/redis"; +const logger = createLogger({ module: "cachedRequestHandler" }); + interface Options> { // Unique key for this request handler queryKey: string; @@ -34,9 +36,10 @@ export const createCachedRequestHandler = options.cacheDuration.asMilliseconds(); if (shouldRequestNewData) { - logger.debug( - `Cached request handler cache miss for channel='${channel.name}' queryKey='${options.queryKey}' reason='${!channelData ? "no data" : "cache expired"}'`, - ); + logger.debug("Cached request handler cache miss", { + channel: channel.name, + queryKey: options.queryKey, + reason: !channelData ? "no data" : "cache expired", + }); return await requestNewDataAsync(); } - logger.debug( - `Cached request handler cache hit for channel='${channel.name}' queryKey='${options.queryKey}' expiresAt='${dayjs(channelData.timestamp).add(options.cacheDuration).toISOString()}'`, - ); + logger.debug("Cached request handler cache hit", { + channel: channel.name, + queryKey: options.queryKey, + expiresAt: dayjs(channelData.timestamp).add(options.cacheDuration).toISOString(), + }); return channelData; }, async invalidateAsync() { - logger.debug( - `Cached request handler invalidating cache channel='${channel.name}' queryKey='${options.queryKey}'`, - ); + logger.debug("Cached request handler invalidating cache", { + channel: channel.name, + queryKey: options.queryKey, + }); await this.getCachedOrUpdatedDataAsync({ forceUpdate: true }); }, subscribe(callback: (data: TData) => void) { diff --git a/packages/request-handler/src/lib/cached-request-integration-job-handler.ts b/packages/request-handler/src/lib/cached-request-integration-job-handler.ts index cb1cda699..dd5249a24 100644 --- a/packages/request-handler/src/lib/cached-request-integration-job-handler.ts +++ b/packages/request-handler/src/lib/cached-request-integration-job-handler.ts @@ -3,10 +3,11 @@ import SuperJSON from "superjson"; import { hashObjectBase64, Stopwatch } from "@homarr/common"; import { decryptSecret } from "@homarr/common/server"; import type { MaybeArray } from "@homarr/common/types"; +import { createLogger } from "@homarr/core/infrastructure/logs"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { db } from "@homarr/db"; import { getItemsWithIntegrationsAsync, getServerSettingsAsync } from "@homarr/db/queries"; import type { WidgetKind } from "@homarr/definitions"; -import { logger } from "@homarr/log"; // This imports are done that way to avoid circular dependencies. import type { inferSupportedIntegrationsStrict } from "../../../widgets/src"; @@ -14,6 +15,8 @@ import { reduceWidgetOptionsWithDefaultValues } from "../../../widgets/src"; import type { WidgetComponentProps } from "../../../widgets/src/definition"; import type { createCachedIntegrationRequestHandler } from "./cached-integration-request-handler"; +const logger = createLogger({ module: "cachedRequestIntegrationJobHandler" }); + export const createRequestIntegrationJobHandler = < TWidgetKind extends WidgetKind, TIntegrationKind extends inferSupportedIntegrationsStrict, @@ -37,9 +40,10 @@ export const createRequestIntegrationJobHandler = < kinds: widgetKinds, }); - logger.debug( - `Found items for integration widgetKinds='${widgetKinds.join(",")}' count=${itemsForIntegration.length}`, - ); + logger.debug("Found items for integration", { + widgetKinds: widgetKinds.join(","), + count: itemsForIntegration.length, + }); const distinctIntegrations: { integrationId: string; @@ -102,14 +106,14 @@ export const createRequestIntegrationJobHandler = < ); const stopWatch = new Stopwatch(); await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); - logger.debug( - `Ran integration job integration=${integrationId} inputHash='${inputHash}' elapsed=${stopWatch.getElapsedInHumanWords()}`, - ); + logger.debug("Ran integration job", { + integration: integrationId, + inputHash, + elapsed: stopWatch.getElapsedInHumanWords(), + }); } catch (error) { logger.error( - new Error(`Failed to run integration job integration=${integrationId} inputHash='${inputHash}'`, { - cause: error, - }), + new ErrorWithMetadata("Failed to run integration job", { integrationId, inputHash }, { cause: error }), ); } } diff --git a/packages/request-handler/src/rss-feeds.ts b/packages/request-handler/src/rss-feeds.ts index 1efdcc151..e1c9f68c5 100644 --- a/packages/request-handler/src/rss-feeds.ts +++ b/packages/request-handler/src/rss-feeds.ts @@ -4,10 +4,12 @@ import dayjs from "dayjs"; import { z } from "zod/v4"; import type { Modify } from "@homarr/common/types"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; +const logger = createLogger({ module: "rssFeedsRequestHandler" }); + export const rssFeedsRequestHandler = createCachedWidgetRequestHandler({ queryKey: "rssFeedList", widgetKind: "rssFeed", diff --git a/packages/request-handler/src/update-checker.ts b/packages/request-handler/src/update-checker.ts index a12e63564..b47d0894e 100644 --- a/packages/request-handler/src/update-checker.ts +++ b/packages/request-handler/src/update-checker.ts @@ -4,12 +4,14 @@ import { compareSemVer, isValidSemVer } from "semver-parser"; import { fetchWithTimeout } from "@homarr/common"; import { env } from "@homarr/common/env"; -import { logger } from "@homarr/log"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { createChannelWithLatestAndEvents } from "@homarr/redis"; import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler"; import packageJson from "../../../package.json"; +const logger = createLogger({ module: "updateCheckerRequestHandler" }); + export const updateCheckerRequestHandler = createCachedRequestHandler({ queryKey: "homarr-update-checker", cacheDuration: dayjs.duration(1, "hour"), @@ -34,7 +36,7 @@ export const updateCheckerRequestHandler = createCachedRequestHandler({ for (const release of releases.data) { if (!isValidSemVer(release.tag_name)) { - logger.warn(`Unable to parse semantic tag '${release.tag_name}'. Update check might not work.`); + logger.warn("Unable to parse semantic tag. Update check might not work.", { tagName: release.tag_name }); continue; } @@ -46,11 +48,12 @@ export const updateCheckerRequestHandler = createCachedRequestHandler({ .sort((releaseA, releaseB) => compareSemVer(releaseB.tag_name, releaseA.tag_name)); if (availableNewerReleases.length > 0) { logger.info( + "Update checker found a new available version", // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - `Update checker found a new available version: ${availableReleases[0]!.tag_name}. Current version is ${currentVersion}`, + { version: availableReleases[0]!.tag_name, currentVersion }, ); } else { - logger.debug(`Update checker did not find any available updates. Current version is ${currentVersion}`); + logger.debug("Update checker did not find any available updates", { currentVersion }); } return { diff --git a/packages/ui/package.json b/packages/ui/package.json index f2bd94550..55c5a2af2 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -27,7 +27,6 @@ "dependencies": { "@homarr/common": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", - "@homarr/log": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "@mantine/core": "^8.3.10", diff --git a/packages/widgets/package.json b/packages/widgets/package.json index 74c29987a..9967de6af 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -31,13 +31,13 @@ "@homarr/auth": "workspace:^0.1.0", "@homarr/boards": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", + "@homarr/core": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", "@homarr/docker": "workspace:^0.1.0", "@homarr/form": "workspace:^0.1.0", "@homarr/forms-collection": "workspace:^0.1.0", "@homarr/integrations": "workspace:^0.1.0", - "@homarr/log": "workspace:^0.1.0", "@homarr/modals": "workspace:^0.1.0", "@homarr/modals-collection": "workspace:^0.1.0", "@homarr/notifications": "workspace:^0.1.0", diff --git a/packages/widgets/src/app/prefetch.ts b/packages/widgets/src/app/prefetch.ts index c1d148a6b..45300197b 100644 --- a/packages/widgets/src/app/prefetch.ts +++ b/packages/widgets/src/app/prefetch.ts @@ -1,10 +1,12 @@ import { trpc } from "@homarr/api/server"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { db, inArray } from "@homarr/db"; import { apps } from "@homarr/db/schema"; -import { logger } from "@homarr/log"; import type { Prefetch } from "../definition"; +const logger = createLogger({ module: "appWidgetPrefetch" }); + const prefetchAllAsync: Prefetch<"app"> = async (queryClient, items) => { const appIds = items.map((item) => item.options.appId); const distinctAppIds = [...new Set(appIds)]; @@ -17,7 +19,7 @@ const prefetchAllAsync: Prefetch<"app"> = async (queryClient, items) => { queryClient.setQueryData(trpc.app.byId.queryKey({ id: app.id }), app); } - logger.info(`Successfully prefetched ${dbApps.length} apps for app widget`); + logger.info("Successfully prefetched apps for app widget", { count: dbApps.length }); }; export default prefetchAllAsync; diff --git a/packages/widgets/src/bookmarks/prefetch.ts b/packages/widgets/src/bookmarks/prefetch.ts index 1da24c6ed..2c3970327 100644 --- a/packages/widgets/src/bookmarks/prefetch.ts +++ b/packages/widgets/src/bookmarks/prefetch.ts @@ -1,10 +1,12 @@ import { trpc } from "@homarr/api/server"; +import { createLogger } from "@homarr/core/infrastructure/logs"; import { db, inArray } from "@homarr/db"; import { apps } from "@homarr/db/schema"; -import { logger } from "@homarr/log"; import type { Prefetch } from "../definition"; +const logger = createLogger({ module: "bookmarksWidgetPrefetch" }); + const prefetchAllAsync: Prefetch<"bookmarks"> = async (queryClient, items) => { const appIds = items.flatMap((item) => item.options.items); const distinctAppIds = [...new Set(appIds)]; @@ -24,7 +26,7 @@ const prefetchAllAsync: Prefetch<"bookmarks"> = async (queryClient, items) => { ); } - logger.info(`Successfully prefetched ${dbApps.length} apps for bookmarks`); + logger.info("Successfully prefetched apps for bookmarks", { count: dbApps.length }); }; export default prefetchAllAsync; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b16340d23..6bc61c0f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -172,9 +172,6 @@ importers: '@homarr/integrations': specifier: workspace:^0.1.0 version: link:../../packages/integrations - '@homarr/log': - specifier: workspace:^ - version: link:../../packages/log '@homarr/modals': specifier: workspace:^0.1.0 version: link:../../packages/modals @@ -389,6 +386,9 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../../packages/common + '@homarr/core': + specifier: workspace:^ + version: link:../../packages/core '@homarr/cron-job-api': specifier: workspace:^0.1.0 version: link:../../packages/cron-job-api @@ -410,9 +410,6 @@ importers: '@homarr/integrations': specifier: workspace:^0.1.0 version: link:../../packages/integrations - '@homarr/log': - specifier: workspace:^ - version: link:../../packages/log '@homarr/ping': specifier: workspace:^0.1.0 version: link:../../packages/ping @@ -489,15 +486,15 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../../packages/common + '@homarr/core': + specifier: workspace:^ + version: link:../../packages/core '@homarr/db': specifier: workspace:^0.1.0 version: link:../../packages/db '@homarr/definitions': specifier: workspace:^0.1.0 version: link:../../packages/definitions - '@homarr/log': - specifier: workspace:^ - version: link:../../packages/log '@homarr/redis': specifier: workspace:^0.1.0 version: link:../../packages/redis @@ -541,12 +538,12 @@ importers: packages/analytics: dependencies: + '@homarr/core': + specifier: workspace:^0.1.0 + version: link:../core '@homarr/db': specifier: workspace:^0.1.0 version: link:../db - '@homarr/log': - specifier: workspace:^0.1.0 - version: link:../log '@homarr/server-settings': specifier: workspace:^0.1.0 version: link:../server-settings @@ -611,9 +608,6 @@ importers: '@homarr/integrations': specifier: workspace:^0.1.0 version: link:../integrations - '@homarr/log': - specifier: workspace:^ - version: link:../log '@homarr/old-import': specifier: workspace:^0.1.0 version: link:../old-import @@ -720,9 +714,6 @@ importers: '@homarr/definitions': specifier: workspace:^0.1.0 version: link:../definitions - '@homarr/log': - specifier: workspace:^0.1.0 - version: link:../log '@homarr/validation': specifier: workspace:^0.1.0 version: link:../validation @@ -877,9 +868,6 @@ importers: '@homarr/core': specifier: workspace:^0.1.0 version: link:../core - '@homarr/log': - specifier: workspace:^0.1.0 - version: link:../log '@paralleldrive/cuid2': specifier: ^3.1.0 version: 3.1.0 @@ -935,6 +923,12 @@ importers: ioredis: specifier: 5.8.2 version: 5.8.2 + superjson: + specifier: 2.2.6 + version: 2.2.6 + winston: + specifier: 3.19.0 + version: 3.19.0 zod: specifier: ^4.1.13 version: 4.1.13 @@ -966,9 +960,6 @@ importers: '@homarr/cron-jobs': specifier: workspace:^0.1.0 version: link:../cron-jobs - '@homarr/log': - specifier: workspace:^0.1.0 - version: link:../log '@tanstack/react-query': specifier: ^5.90.12 version: 5.90.12(react@19.2.2) @@ -1046,6 +1037,9 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../common + '@homarr/core': + specifier: workspace:^0.1.0 + version: link:../core '@homarr/cron-job-status': specifier: workspace:^0.1.0 version: link:../cron-job-status @@ -1064,9 +1058,6 @@ importers: '@homarr/integrations': specifier: workspace:^0.1.0 version: link:../integrations - '@homarr/log': - specifier: workspace:^0.1.0 - version: link:../log '@homarr/ping': specifier: workspace:^0.1.0 version: link:../ping @@ -1110,6 +1101,9 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../common + '@homarr/core': + specifier: workspace:^ + version: link:../core '@homarr/db': specifier: workspace:^0.1.0 version: link:../db @@ -1150,9 +1144,6 @@ importers: '@homarr/definitions': specifier: workspace:^0.1.0 version: link:../definitions - '@homarr/log': - specifier: workspace:^0.1.0 - version: link:../log '@homarr/server-settings': specifier: workspace:^0.1.0 version: link:../server-settings @@ -1383,12 +1374,12 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../common + '@homarr/core': + specifier: workspace:^0.1.0 + version: link:../core '@homarr/db': specifier: workspace:^0.1.0 version: link:../db - '@homarr/log': - specifier: workspace:^0.1.0 - version: link:../log devDependencies: '@homarr/eslint-config': specifier: workspace:^0.2.0 @@ -1414,9 +1405,9 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../common - '@homarr/log': + '@homarr/core': specifier: workspace:^0.1.0 - version: link:../log + version: link:../core '@homarr/redis': specifier: workspace:^0.1.0 version: link:../redis @@ -1463,6 +1454,9 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../common + '@homarr/core': + specifier: workspace:^0.1.0 + version: link:../core '@homarr/db': specifier: workspace:^0.1.0 version: link:../db @@ -1472,9 +1466,6 @@ importers: '@homarr/image-proxy': specifier: workspace:^0.1.0 version: link:../image-proxy - '@homarr/log': - specifier: workspace:^0.1.0 - version: link:../log '@homarr/node-unifi': specifier: ^2.6.0 version: 2.6.0(undici@7.16.0) @@ -1543,37 +1534,6 @@ importers: specifier: ^5.9.3 version: 5.9.3 - packages/log: - dependencies: - '@homarr/core': - specifier: workspace:^0.1.0 - version: link:../core - superjson: - specifier: 2.2.6 - version: 2.2.6 - winston: - specifier: 3.19.0 - version: 3.19.0 - zod: - specifier: ^4.1.13 - version: 4.1.13 - 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.39.1 - version: 9.39.1 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - packages/modals: dependencies: '@homarr/translation': @@ -1714,6 +1674,9 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../common + '@homarr/core': + specifier: workspace:^0.1.0 + version: link:../core '@homarr/db': specifier: workspace:^0.1.0 version: link:../db @@ -1723,9 +1686,6 @@ importers: '@homarr/form': specifier: workspace:^0.1.0 version: link:../form - '@homarr/log': - specifier: workspace:^0.1.0 - version: link:../log '@homarr/modals': specifier: workspace:^0.1.0 version: link:../modals @@ -1824,9 +1784,9 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../common - '@homarr/log': + '@homarr/core': specifier: workspace:^0.1.0 - version: link:../log + version: link:../core devDependencies: '@homarr/eslint-config': specifier: workspace:^0.2.0 @@ -1858,9 +1818,6 @@ importers: '@homarr/definitions': specifier: workspace:^ version: link:../definitions - '@homarr/log': - specifier: workspace:^ - version: link:../log ioredis: specifier: 5.8.2 version: 5.8.2 @@ -1892,6 +1849,9 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../common + '@homarr/core': + specifier: workspace:^0.1.0 + version: link:../core '@homarr/db': specifier: workspace:^0.1.0 version: link:../db @@ -1904,9 +1864,6 @@ importers: '@homarr/integrations': specifier: workspace:^0.1.0 version: link:../integrations - '@homarr/log': - specifier: workspace:^0.1.0 - version: link:../log '@homarr/redis': specifier: workspace:^0.1.0 version: link:../redis @@ -2137,9 +2094,6 @@ importers: '@homarr/definitions': specifier: workspace:^0.1.0 version: link:../definitions - '@homarr/log': - specifier: workspace:^0.1.0 - version: link:../log '@homarr/translation': specifier: workspace:^0.1.0 version: link:../translation @@ -2244,6 +2198,9 @@ importers: '@homarr/common': specifier: workspace:^0.1.0 version: link:../common + '@homarr/core': + specifier: workspace:^0.1.0 + version: link:../core '@homarr/db': specifier: workspace:^0.1.0 version: link:../db @@ -2262,9 +2219,6 @@ importers: '@homarr/integrations': specifier: workspace:^0.1.0 version: link:../integrations - '@homarr/log': - specifier: workspace:^0.1.0 - version: link:../log '@homarr/modals': specifier: workspace:^0.1.0 version: link:../modals