feat: Add centralized logging system with @link-stack/logger package

- Create new @link-stack/logger package wrapping Pino for structured logging
- Replace all console.log/error/warn statements across the monorepo
- Configure environment-aware logging (pretty-print in dev, JSON in prod)
- Add automatic redaction of sensitive fields (passwords, tokens, etc.)
- Remove dead commented-out logger file from bridge-worker
- Follow Pino's standard argument order (context object first, message second)
- Support log levels via LOG_LEVEL environment variable
- Export TypeScript types for better IDE support

This provides consistent, structured logging across all applications
and packages, making debugging easier and production logs more parseable.
This commit is contained in:
Darren Clarke 2025-08-20 11:37:39 +02:00
parent 5b89bfce7c
commit c1feaa4cb1
42 changed files with 3824 additions and 2422 deletions

View file

@ -1,6 +1,9 @@
import { Metadata } from "next";
import { redirect } from "next/navigation";
import { Home } from "./_components/Home";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('link-page');
// import { getServerSession } from "app/_lib/authentication";
// import { Home } from "@link-stack/leafcutter-ui";
// import { getUserVisualizations } from "@link-stack/opensearch-common";
@ -28,7 +31,7 @@ export default async function Page() {
try {
visualizations = await getUserVisualizations(email ?? "none", 20);
} catch (e) {
console.error(e.meta);
logger.error({ meta: e.meta }, "Error metadata");
}
return (

View file

@ -1,6 +1,9 @@
"use server";
import { executeREST } from "app/_lib/zammad";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('link-groups');
export const getGroupsAction = async () => {
try {
@ -15,7 +18,7 @@ export const getGroupsAction = async () => {
return formattedGroups;
} catch (e) {
console.error(e.message);
logger.error({ error: e }, "Error occurred");
return [];
}
};

View file

@ -3,6 +3,9 @@
import { executeGraphQL, executeREST } from "app/_lib/zammad";
import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery";
import { getTicketsByOverviewQuery } from "app/_graphql/getTicketsByOverviewQuery";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('link-overviews');
const overviewLookup = {
Assigned: "My Assigned Tickets",
@ -36,7 +39,7 @@ export const getOverviewTicketCountsAction = async () => {
return counts;
} catch (e) {
console.error(e.message);
logger.error({ error: e }, "Error occurred");
return {};
}
};
@ -91,7 +94,7 @@ export const getOverviewTicketsAction = async (name: string) => {
return { tickets: sortedTickets };
} catch (e) {
console.error(e.message);
logger.error({ error: e }, "Error occurred");
return { tickets, message: e.message ?? "" };
}
};

View file

@ -1,6 +1,9 @@
"use server";
import { executeGraphQL } from "app/_lib/zammad";
import { searchQuery } from "@/app/_graphql/searchQuery";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('link-search');
export const searchAllAction = async (query: string, limit: number) => {
try {
@ -11,7 +14,7 @@ export const searchAllAction = async (query: string, limit: number) => {
return result?.search;
} catch (e) {
console.error(e.message);
logger.error({ error: e }, "Error occurred");
return [];
}
};

View file

@ -6,6 +6,9 @@ import { createTicketMutation } from "app/_graphql/createTicketMutation";
import { updateTicketMutation } from "app/_graphql/updateTicketMutation";
import { updateTagsMutation } from "app/_graphql/updateTagsMutation";
import { executeGraphQL, executeREST } from "app/_lib/zammad";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('link-tickets');
export const createTicketAction = async (
currentState: any,
@ -35,7 +38,7 @@ export const createTicketAction = async (
success: true,
};
} catch (e: any) {
console.error(e.message);
logger.error({ error: e }, "Error occurred");
return {
success: false,
values: {},
@ -62,7 +65,7 @@ export const createTicketArticleAction = async (
success: true,
};
} catch (e: any) {
console.error(e.message);
logger.error({ error: e }, "Error occurred");
return {
success: false,
message: e?.message ?? "Unknown error",
@ -115,7 +118,7 @@ export const updateTicketAction = async (
success: true,
};
} catch (e: any) {
console.error(e.message);
logger.error({ error: e }, "Error occurred");
return {
success: false,
message: e?.message ?? "Unknown error",
@ -132,7 +135,7 @@ export const getTicketAction = async (id: string) => {
return ticketData?.ticket;
} catch (e) {
console.error(e.message);
logger.error({ error: e }, "Error occurred");
return {};
}
};
@ -146,7 +149,7 @@ export const getTicketArticlesAction = async (id: string) => {
return ticketData?.ticketArticles;
} catch (e) {
console.error(e.message);
logger.error({ error: e }, "Error occurred");
return {};
}
};
@ -164,7 +167,7 @@ export const getTicketStatesAction = async () => {
})) ?? [];
return formattedStates;
} catch (e) {
console.error(e.message);
logger.error({ error: e }, "Error occurred");
return [];
}
};
@ -177,7 +180,7 @@ export const getTagsAction = async () => {
return tags;
} catch (e) {
console.error(e.message);
logger.error({ error: e }, "Error occurred");
return [];
}
};
@ -196,7 +199,7 @@ export const getTicketPrioritiesAction = async () => {
return formattedPriorities;
} catch (e) {
console.error(e.message);
logger.error({ error: e }, "Error occurred");
return [];
}
};

View file

@ -1,6 +1,9 @@
"use server";
import { executeREST } from "app/_lib/zammad";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('link-users');
export const getAgentsAction = async (groupID: number) => {
try {
@ -21,7 +24,7 @@ export const getAgentsAction = async (groupID: number) => {
return formattedAgents;
} catch (e) {
console.error(e.message);
logger.error({ error: e }, "Error occurred");
return [];
}
};
@ -42,7 +45,7 @@ export const getCustomersAction = async () => {
return formattedCustomers;
} catch (e) {
console.error(e.message);
logger.error({ error: e }, "Error occurred");
return [];
}
};
@ -61,7 +64,7 @@ export const getUsersAction = async () => {
return formattedUsers;
} catch (e) {
console.error(e.message);
logger.error({ error: e }, "Error occurred");
return [];
}
};

View file

@ -12,6 +12,9 @@ import Credentials from "next-auth/providers/credentials";
import Apple from "next-auth/providers/apple";
import { Redis } from "ioredis";
import AzureADProvider from "next-auth/providers/azure-ad";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('link-authentication');
const headers = { Authorization: `Token ${process.env.ZAMMAD_API_TOKEN}` };
@ -48,7 +51,7 @@ const getUserRoles = async (email: string) => {
});
return roles.filter((role: string) => role !== null);
} catch (e) {
console.error({ e });
logger.error({ error: e, email }, 'Failed to get user roles');
return [];
}
};

View file

@ -1,3 +1,7 @@
import { createLogger } from "@link-stack/logger";
const logger = createLogger('link-utils');
export const fetchLeafcutter = async (url: string, options: any) => {
/*
@ -13,7 +17,7 @@ export const fetchLeafcutter = async (url: string, options: any) => {
const json = await res.json();
return json;
} catch (error) {
console.error({ error });
logger.error({ error }, "Error occurred");
return null;
}
};

View file

@ -1,5 +1,8 @@
import { NextResponse } from "next/server";
import { withAuth, NextRequestWithAuth } from "next-auth/middleware";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('link-middleware');
const rewriteURL = (
request: NextRequestWithAuth,
@ -7,14 +10,17 @@ const rewriteURL = (
destinationBaseURL: string,
headers: any = {},
) => {
console.log("Rewriting URL");
console.log({ request, originBaseURL, destinationBaseURL, headers });
logger.debug({
originBaseURL,
destinationBaseURL,
headerKeys: Object.keys(headers)
}, "Rewriting URL");
let path = request.url.replace(originBaseURL, "");
if (path.startsWith("/")) {
path = path.slice(1);
}
const destinationURL = `${destinationBaseURL}/${path}`;
console.info(`Rewriting ${request.url} to ${destinationURL}`);
logger.debug({ from: request.url, to: destinationURL }, "URL rewrite");
const requestHeaders = new Headers(request.headers);
requestHeaders.delete("x-forwarded-user");
@ -32,7 +38,7 @@ const rewriteURL = (
const checkRewrites = async (request: NextRequestWithAuth) => {
const linkBaseURL = process.env.LINK_URL ?? "http://localhost:3000";
console.log({ linkBaseURL });
logger.debug({ linkBaseURL }, "Link base URL");
const opensearchBaseURL =
process.env.OPENSEARCH_DASHBOARDS_URL ??
"http://opensearch-dashboards:5601";

View file

@ -19,6 +19,7 @@
"@link-stack/bridge-common": "*",
"@link-stack/bridge-ui": "*",
"@link-stack/leafcutter-ui": "*",
"@link-stack/logger": "*",
"@link-stack/opensearch-common": "*",
"@link-stack/ui": "*",
"@mui/icons-material": "^6",