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

@ -10,6 +10,9 @@ import {
CamelCasePlugin, CamelCasePlugin,
} from "kysely"; } from "kysely";
import pkg from "pg"; import pkg from "pg";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('bridge-migrations-migrate');
const { Pool } = pkg; const { Pool } = pkg;
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
@ -72,17 +75,17 @@ export const migrate = async (arg: string) => {
results?.forEach((it) => { results?.forEach((it) => {
if (it.status === "Success") { if (it.status === "Success") {
console.info( logger.info(
`Migration "${it.migrationName} ${it.direction.toLowerCase()}" was executed successfully`, `Migration "${it.migrationName} ${it.direction.toLowerCase()}" was executed successfully`,
); );
} else if (it.status === "Error") { } else if (it.status === "Error") {
console.error(`Failed to execute migration "${it.migrationName}"`); logger.error(`Failed to execute migration "${it.migrationName}"`);
} }
}); });
if (error) { if (error) {
console.error("Failed to migrate"); logger.error("Failed to migrate");
console.error(error); logger.error(error);
process.exit(1); process.exit(1);
} }

View file

@ -9,6 +9,7 @@
"migrate:down:one": "tsx migrate.ts down:one" "migrate:down:one": "tsx migrate.ts down:one"
}, },
"dependencies": { "dependencies": {
"@link-stack/logger": "*",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"kysely": "0.27.6", "kysely": "0.27.6",
"pg": "^8.14.1", "pg": "^8.14.1",

View file

@ -9,6 +9,9 @@ import {
SendMessageRoute, SendMessageRoute,
ReceiveMessageRoute, ReceiveMessageRoute,
} from "./routes.js"; } from "./routes.js";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('bridge-whatsapp-index');
const server = Hapi.server({ port: 5000 }); const server = Hapi.server({ port: 5000 });
@ -34,6 +37,6 @@ const main = async () => {
}; };
main().catch((err) => { main().catch((err) => {
console.error(err); logger.error(err);
process.exit(1); process.exit(1);
}); });

View file

@ -11,6 +11,9 @@ import makeWASocket, {
useMultiFileAuthState, useMultiFileAuthState,
} from "@whiskeysockets/baileys"; } from "@whiskeysockets/baileys";
import fs from "fs"; import fs from "fs";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('bridge-whatsapp-service');
export type AuthCompleteCallback = (error?: string) => void; export type AuthCompleteCallback = (error?: string) => void;
@ -57,7 +60,7 @@ export default class WhatsappService extends Service {
try { try {
connection.end(null); connection.end(null);
} catch (error) { } catch (error) {
console.error(error); logger.error({ error }, 'Connection reset error');
} }
} }
this.connections = {}; this.connections = {};
@ -92,27 +95,27 @@ export default class WhatsappService extends Service {
isNewLogin, isNewLogin,
} = update; } = update;
if (qr) { if (qr) {
console.info("got qr code"); logger.info('got qr code');
const botDirectory = this.getBotDirectory(botID); const botDirectory = this.getBotDirectory(botID);
const qrPath = `${botDirectory}/qr.txt`; const qrPath = `${botDirectory}/qr.txt`;
fs.writeFileSync(qrPath, qr, "utf8"); fs.writeFileSync(qrPath, qr, "utf8");
} else if (isNewLogin) { } else if (isNewLogin) {
console.info("got new login"); logger.info('got new login');
const botDirectory = this.getBotDirectory(botID); const botDirectory = this.getBotDirectory(botID);
const verifiedFile = `${botDirectory}/verified`; const verifiedFile = `${botDirectory}/verified`;
fs.writeFileSync(verifiedFile, ""); fs.writeFileSync(verifiedFile, "");
} else if (connectionState === "open") { } else if (connectionState === "open") {
console.info("opened connection"); logger.info('opened connection');
} else if (connectionState === "close") { } else if (connectionState === "close") {
console.info("connection closed due to ", lastDisconnect?.error); logger.info({ lastDisconnect }, 'connection closed');
const disconnectStatusCode = (lastDisconnect?.error as any)?.output const disconnectStatusCode = (lastDisconnect?.error as any)?.output
?.statusCode; ?.statusCode;
if (disconnectStatusCode === DisconnectReason.restartRequired) { if (disconnectStatusCode === DisconnectReason.restartRequired) {
console.info("reconnecting after got new login"); logger.info('reconnecting after got new login');
await this.createConnection(botID, server, options); await this.createConnection(botID, server, options);
authCompleteCallback?.(); authCompleteCallback?.();
} else if (disconnectStatusCode !== DisconnectReason.loggedOut) { } else if (disconnectStatusCode !== DisconnectReason.loggedOut) {
console.info("reconnecting"); logger.info('reconnecting');
await this.sleep(pause); await this.sleep(pause);
pause *= 2; pause *= 2;
this.createConnection(botID, server, options); this.createConnection(botID, server, options);
@ -121,12 +124,12 @@ export default class WhatsappService extends Service {
} }
if (events["creds.update"]) { if (events["creds.update"]) {
console.info("creds update"); logger.info('creds update');
await saveCreds(); await saveCreds();
} }
if (events["messages.upsert"]) { if (events["messages.upsert"]) {
console.info("messages upsert"); logger.info('messages upsert');
const upsert = events["messages.upsert"]; const upsert = events["messages.upsert"];
const { messages } = upsert; const { messages } = upsert;
if (messages) { if (messages) {
@ -149,7 +152,7 @@ export default class WhatsappService extends Service {
const verifiedFile = `${directory}/verified`; const verifiedFile = `${directory}/verified`;
if (fs.existsSync(verifiedFile)) { if (fs.existsSync(verifiedFile)) {
const { version, isLatest } = await fetchLatestBaileysVersion(); const { version, isLatest } = await fetchLatestBaileysVersion();
console.info(`using WA v${version.join(".")}, isLatest: ${isLatest}`); logger.info({ version: version.join('.'), isLatest }, 'using WA version');
await this.createConnection(botID, this.server, { await this.createConnection(botID, this.server, {
browser: WhatsappService.browserDescription, browser: WhatsappService.browserDescription,
@ -169,9 +172,9 @@ export default class WhatsappService extends Service {
message, message,
messageTimestamp, messageTimestamp,
} = webMessageInfo; } = webMessageInfo;
console.info("Message type debug"); logger.info('Message type debug');
for (const key in message) { for (const key in message) {
console.info(key, !!message[key as keyof proto.IMessage]); logger.info({ key, exists: !!message[key as keyof proto.IMessage] }, 'Message field');
} }
const isValidMessage = const isValidMessage =
message && remoteJid !== "status@broadcast" && !fromMe; message && remoteJid !== "status@broadcast" && !fromMe;

View file

@ -1,12 +1,14 @@
import { run } from "graphile-worker"; import { run } from "graphile-worker";
import { createLogger } from "@link-stack/logger";
import * as path from "path"; import * as path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
const logger = createLogger('bridge-worker');
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const startWorker = async () => { const startWorker = async () => {
console.info("Starting worker..."); logger.info("Starting worker...");
await run({ await run({
connectionString: process.env.DATABASE_URL, connectionString: process.env.DATABASE_URL,
@ -30,6 +32,6 @@ const main = async () => {
}; };
main().catch((err) => { main().catch((err) => {
console.error(err); logger.error({ error: err }, 'Worker failed to start');
process.exit(1); process.exit(1);
}); });

View file

@ -3,6 +3,9 @@
import Twilio from "twilio"; import Twilio from "twilio";
import { CallInstance } from "twilio/lib/rest/api/v2010/account/call"; import { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
import { Zammad, getOrCreateUser } from "./zammad.js"; import { Zammad, getOrCreateUser } from "./zammad.js";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('bridge-worker-common');
type SavedVoiceProvider = any; type SavedVoiceProvider = any;
@ -63,7 +66,7 @@ export const createZammadTicket = async (
}); });
} catch (error: any) { } catch (error: any) {
if (error.isBoom) { if (error.isBoom) {
console.error(error.output); logger.error({ output: error.output }, 'Zammad ticket creation failed');
throw new Error("Failed to create zamamd ticket"); throw new Error("Failed to create zamamd ticket");
} }
} }

View file

@ -1,11 +0,0 @@
//import { defState } from "@digiresilience/montar";
//import { configureLogger } from "@digiresilience/bridge-common";
// import config from "@digiresilience/bridge-config";
//export const logger = defState("workerLogger", {
// start: async () => configureLogger(config),
//});
//export default logger;
export const logger = {};
export default logger;

View file

@ -1,6 +1,9 @@
import { Readable } from "stream"; import { Readable } from "stream";
import ffmpeg from "fluent-ffmpeg"; import ffmpeg from "fluent-ffmpeg";
import * as R from "remeda"; import * as R from "remeda";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('bridge-worker-media-convert');
const requiredCodecs = ["mp3", "webm", "wav"]; const requiredCodecs = ["mp3", "webm", "wav"];
@ -36,7 +39,7 @@ export const convert = (
.audioBitrate(settings.bitrate) .audioBitrate(settings.bitrate)
.toFormat(settings.format) .toFormat(settings.format)
.on("error", (err, _stdout, _stderr) => { .on("error", (err, _stdout, _stderr) => {
console.error(err.message); logger.error({ error: err }, 'FFmpeg conversion error');
reject(err); reject(err);
}) })
.on("end", () => { .on("end", () => {
@ -58,7 +61,7 @@ export const selfCheck = (): Promise<boolean> => {
return new Promise((resolve) => { return new Promise((resolve) => {
ffmpeg.getAvailableFormats((err, codecs) => { ffmpeg.getAvailableFormats((err, codecs) => {
if (err) { if (err) {
console.error("FFMPEG error:", err); logger.error({ error: err }, 'FFMPEG error');
resolve(false); resolve(false);
} }

View file

@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"@hapi/wreck": "^18.1.0", "@hapi/wreck": "^18.1.0",
"@link-stack/bridge-common": "*", "@link-stack/bridge-common": "*",
"@link-stack/logger": "*",
"@link-stack/signal-api": "*", "@link-stack/signal-api": "*",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"graphile-worker": "^0.16.6", "graphile-worker": "^0.16.6",

View file

@ -1,4 +1,7 @@
import { db } from "@link-stack/bridge-common"; import { db } from "@link-stack/bridge-common";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('notify-webhooks');
export interface NotifyWebhooksOptions { export interface NotifyWebhooksOptions {
backendId: string; backendId: string;
@ -10,11 +13,10 @@ const notifyWebhooksTask = async (
): Promise<void> => { ): Promise<void> => {
const { backendId, payload } = options; const { backendId, payload } = options;
console.log(`[notify-webhooks] Processing webhook notification:`, { logger.debug({
backendId, backendId,
payloadKeys: Object.keys(payload), payloadKeys: Object.keys(payload),
payload: JSON.stringify(payload, null, 2), }, 'Processing webhook notification');
});
const webhooks = await db const webhooks = await db
.selectFrom("Webhook") .selectFrom("Webhook")
@ -22,20 +24,19 @@ const notifyWebhooksTask = async (
.where("backendId", "=", backendId) .where("backendId", "=", backendId)
.execute(); .execute();
console.log(`[notify-webhooks] Found ${webhooks.length} webhooks for backend ${backendId}`); logger.debug({ count: webhooks.length, backendId }, 'Found webhooks');
for (const webhook of webhooks) { for (const webhook of webhooks) {
const { endpointUrl, httpMethod, headers } = webhook; const { endpointUrl, httpMethod, headers } = webhook;
const finalHeaders = { "Content-Type": "application/json", ...headers }; const finalHeaders = { "Content-Type": "application/json", ...headers };
const body = JSON.stringify(payload); const body = JSON.stringify(payload);
console.log(`[notify-webhooks] Sending webhook:`, { logger.debug({
url: endpointUrl, url: endpointUrl,
method: httpMethod, method: httpMethod,
bodyLength: body.length, bodyLength: body.length,
headers: Object.keys(finalHeaders), headerKeys: Object.keys(finalHeaders),
payload: body, }, 'Sending webhook');
});
try { try {
const result = await fetch(endpointUrl, { const result = await fetch(endpointUrl, {
@ -44,26 +45,26 @@ const notifyWebhooksTask = async (
body, body,
}); });
console.log(`[notify-webhooks] Webhook response:`, { logger.debug({
url: endpointUrl, url: endpointUrl,
status: result.status, status: result.status,
statusText: result.statusText, statusText: result.statusText,
ok: result.ok, ok: result.ok,
}); }, 'Webhook response');
if (!result.ok) { if (!result.ok) {
const responseText = await result.text(); const responseText = await result.text();
console.error(`[notify-webhooks] Webhook error response:`, { logger.error({
url: endpointUrl, url: endpointUrl,
status: result.status, status: result.status,
response: responseText.substring(0, 500), // First 500 chars responseSample: responseText.substring(0, 500),
}); }, 'Webhook error response');
} }
} catch (error) { } catch (error) {
console.error(`[notify-webhooks] Webhook request failed:`, { logger.error({
url: endpointUrl, url: endpointUrl,
error: error instanceof Error ? error.message : error, error: error instanceof Error ? error.message : error,
}); }, 'Webhook request failed');
} }
} }
}; };

View file

@ -1,4 +1,7 @@
import { db } from "@link-stack/bridge-common"; import { db } from "@link-stack/bridge-common";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('bridge-worker-send-facebook-message');
interface SendFacebookMessageTaskOptions { interface SendFacebookMessageTaskOptions {
token: string; token: string;
@ -32,7 +35,7 @@ const sendFacebookMessageTask = async (
body: JSON.stringify(outgoingMessage), body: JSON.stringify(outgoingMessage),
}); });
} catch (error) { } catch (error) {
console.error({ error }); logger.error({ error });
throw error; throw error;
} }
}; };

View file

@ -1,6 +1,9 @@
import { db, getWorkerUtils } from "@link-stack/bridge-common"; import { db, getWorkerUtils } from "@link-stack/bridge-common";
import { createLogger } from "@link-stack/logger";
import * as signalApi from "@link-stack/signal-api"; import * as signalApi from "@link-stack/signal-api";
const logger = createLogger('fetch-signal-messages');
const { Configuration, MessagesApi, AttachmentsApi } = signalApi; const { Configuration, MessagesApi, AttachmentsApi } = signalApi;
const config = new Configuration({ const config = new Configuration({
basePath: process.env.BRIDGE_SIGNAL_URL, basePath: process.env.BRIDGE_SIGNAL_URL,
@ -59,8 +62,7 @@ const processMessage = async ({
const { attachments } = dataMessage; const { attachments } = dataMessage;
const rawTimestamp = dataMessage?.timestamp; const rawTimestamp = dataMessage?.timestamp;
// Debug logging for group detection logger.debug({
console.log(`[fetch-signal-messages] Processing message:`, {
sourceUuid, sourceUuid,
source, source,
rawTimestamp, rawTimestamp,
@ -71,7 +73,7 @@ const processMessage = async ({
groupV2Id: dataMessage?.groupV2?.id, groupV2Id: dataMessage?.groupV2?.id,
groupContextType: dataMessage?.groupContext?.type, groupContextType: dataMessage?.groupContext?.type,
groupInfoType: dataMessage?.groupInfo?.type, groupInfoType: dataMessage?.groupInfo?.type,
}); }, 'Processing message');
const timestamp = new Date(rawTimestamp); const timestamp = new Date(rawTimestamp);
const formattedAttachments = await fetchAttachments(attachments); const formattedAttachments = await fetchAttachments(attachments);
@ -148,9 +150,7 @@ const fetchSignalMessagesTask = async ({
number: phoneNumber, number: phoneNumber,
}); });
console.log( logger.debug({ botId: id, phoneNumber }, 'Fetching messages for bot');
`[fetch-signal-messages] Fetching messages for bot ${id} (${phoneNumber})`,
);
for (const message of messages) { for (const message of messages) {
const formattedMessages = await processMessage({ const formattedMessages = await processMessage({
@ -160,14 +160,14 @@ const fetchSignalMessagesTask = async ({
}); });
for (const formattedMessage of formattedMessages) { for (const formattedMessage of formattedMessages) {
if (formattedMessage.to !== formattedMessage.from) { if (formattedMessage.to !== formattedMessage.from) {
console.log(`[fetch-signal-messages] Creating job for message:`, { logger.debug({
messageId: formattedMessage.messageId, messageId: formattedMessage.messageId,
from: formattedMessage.from, from: formattedMessage.from,
to: formattedMessage.to, to: formattedMessage.to,
isGroup: formattedMessage.isGroup, isGroup: formattedMessage.isGroup,
hasMessage: !!formattedMessage.message, hasMessage: !!formattedMessage.message,
hasAttachment: !!formattedMessage.attachment, hasAttachment: !!formattedMessage.attachment,
}); }, 'Creating job for message');
await worker.addJob( await worker.addJob(
"signal/receive-signal-message", "signal/receive-signal-message",

View file

@ -2,6 +2,9 @@
/* /*
import { URLSearchParams } from "url"; import { URLSearchParams } from "url";
import { withDb, AppDatabase } from "../../lib/db.js"; import { withDb, AppDatabase } from "../../lib/db.js";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('bridge-worker-import-leafcutter');
// import { loadConfig } from "@digiresilience/bridge-config"; // import { loadConfig } from "@digiresilience/bridge-config";
const config: any = {}; const config: any = {};
@ -122,7 +125,7 @@ const sendToLeafcutter = async (tickets: LabelStudioTicket[]) => {
}; };
}); });
console.info("Sending to Leafcutter"); logger.info("Sending to Leafcutter");
const result = await fetch(opensearchApiUrl, { const result = await fetch(opensearchApiUrl, {
method: "POST", method: "POST",

View file

@ -1,7 +1,10 @@
import { db, getWorkerUtils } from "@link-stack/bridge-common"; import { db, getWorkerUtils } from "@link-stack/bridge-common";
import { createLogger } from "@link-stack/logger";
import * as signalApi from "@link-stack/signal-api"; import * as signalApi from "@link-stack/signal-api";
const { Configuration, GroupsApi } = signalApi; const { Configuration, GroupsApi } = signalApi;
const logger = createLogger('bridge-worker-receive-signal-message');
interface ReceiveSignalMessageTaskOptions { interface ReceiveSignalMessageTaskOptions {
token: string; token: string;
to: string; to: string;
@ -27,7 +30,7 @@ const receiveSignalMessageTask = async ({
mimeType, mimeType,
isGroup, isGroup,
}: ReceiveSignalMessageTaskOptions): Promise<void> => { }: ReceiveSignalMessageTaskOptions): Promise<void> => {
console.log(`[receive-signal-message] Processing incoming message:`, { logger.debug({
messageId, messageId,
from, from,
to, to,
@ -35,7 +38,7 @@ const receiveSignalMessageTask = async ({
hasMessage: !!message, hasMessage: !!message,
hasAttachment: !!attachment, hasAttachment: !!attachment,
token, token,
}); }, 'Processing incoming message');
const worker = await getWorkerUtils(); const worker = await getWorkerUtils();
const row = await db const row = await db
.selectFrom("SignalBot") .selectFrom("SignalBot")
@ -50,20 +53,18 @@ const receiveSignalMessageTask = async ({
// Check if auto-group creation is enabled and this is NOT already a group message // Check if auto-group creation is enabled and this is NOT already a group message
const enableAutoGroups = process.env.BRIDGE_SIGNAL_AUTO_GROUPS === "true"; const enableAutoGroups = process.env.BRIDGE_SIGNAL_AUTO_GROUPS === "true";
console.log(`[receive-signal-message] Auto-groups config:`, { logger.debug({
enableAutoGroups, enableAutoGroups,
isGroup, isGroup,
shouldCreateGroup: enableAutoGroups && !isGroup && from && to, shouldCreateGroup: enableAutoGroups && !isGroup && from && to,
}); }, 'Auto-groups config');
// If this is already a group message and auto-groups is enabled, // If this is already a group message and auto-groups is enabled,
// use group provided in 'to' // use group provided in 'to'
if (enableAutoGroups && isGroup && to) { if (enableAutoGroups && isGroup && to) {
// Signal sends the internal ID (base64) in group messages // Signal sends the internal ID (base64) in group messages
// We should NOT add "group." prefix - that's for sending messages, not receiving // We should NOT add "group." prefix - that's for sending messages, not receiving
console.log( logger.debug('Message is from existing group with internal ID');
`[receive-signal-message] Message is from existing group with internal ID`,
);
finalTo = to; finalTo = to;
} else if (enableAutoGroups && !isGroup && from && to) { } else if (enableAutoGroups && !isGroup && from && to) {
@ -75,9 +76,7 @@ const receiveSignalMessageTask = async ({
// Always create a new group for direct messages to the helpdesk // Always create a new group for direct messages to the helpdesk
// This ensures each conversation gets its own group/ticket // This ensures each conversation gets its own group/ticket
console.log( logger.info({ from }, 'Creating new group for user');
`[receive-signal-message] Creating new group for user ${from}`,
);
// Include timestamp to make each group unique // Include timestamp to make each group unique
const timestamp = new Date() const timestamp = new Date()
@ -96,10 +95,7 @@ const receiveSignalMessageTask = async ({
}, },
}); });
console.log( logger.debug({ createGroupResponse }, 'Group creation response from Signal API');
`[receive-signal-message] Full group creation response from Signal API:`,
JSON.stringify(createGroupResponse, null, 2),
);
if (createGroupResponse.id) { if (createGroupResponse.id) {
// The createGroupResponse.id already contains the full group identifier (group.BASE64) // The createGroupResponse.id already contains the full group identifier (group.BASE64)
@ -108,31 +104,21 @@ const receiveSignalMessageTask = async ({
// Fetch the group details to get the actual internalId // Fetch the group details to get the actual internalId
// The base64 part of the ID is NOT the same as the internalId! // The base64 part of the ID is NOT the same as the internalId!
try { try {
console.log( logger.debug('Fetching group details to get internalId');
`[receive-signal-message] Fetching group details to get internalId...`,
);
const groups = await groupsClient.v1GroupsNumberGet({ const groups = await groupsClient.v1GroupsNumberGet({
number: row.phoneNumber, number: row.phoneNumber,
}); });
console.log( logger.debug({ groupsSample: groups.slice(0, 3) }, 'Groups for bot');
`[receive-signal-message] All groups for bot:`,
JSON.stringify(groups.slice(0, 3), null, 2), // Show first 3 to avoid too much output
);
const createdGroup = groups.find((g) => g.id === finalTo); const createdGroup = groups.find((g) => g.id === finalTo);
if (createdGroup) { if (createdGroup) {
console.log( logger.debug({ createdGroup }, 'Found created group details');
`[receive-signal-message] Found created group details:`,
JSON.stringify(createdGroup, null, 2),
);
} }
if (createdGroup && createdGroup.internalId) { if (createdGroup && createdGroup.internalId) {
createdInternalId = createdGroup.internalId; createdInternalId = createdGroup.internalId;
console.log( logger.debug({ createdInternalId }, 'Got actual internalId');
`[receive-signal-message] Got actual internalId: ${createdInternalId}`,
);
} else { } else {
// Fallback: extract base64 part from ID // Fallback: extract base64 part from ID
if (finalTo.startsWith("group.")) { if (finalTo.startsWith("group.")) {
@ -140,36 +126,32 @@ const receiveSignalMessageTask = async ({
} }
} }
} catch (fetchError) { } catch (fetchError) {
console.log( logger.debug('Could not fetch group details, using ID base64 part');
`[receive-signal-message] Could not fetch group details, using ID base64 part`,
);
// Fallback: extract base64 part from ID // Fallback: extract base64 part from ID
if (finalTo.startsWith("group.")) { if (finalTo.startsWith("group.")) {
createdInternalId = finalTo.substring(6); createdInternalId = finalTo.substring(6);
} }
} }
console.log(`[receive-signal-message] Group created successfully:`, { logger.debug({
fullGroupId: finalTo, fullGroupId: finalTo,
internalId: createdInternalId, internalId: createdInternalId,
}); }, 'Group created successfully');
console.log(`[receive-signal-message] Created new Signal group:`, { logger.debug({
groupId: finalTo, groupId: finalTo,
internalId: createdInternalId, internalId: createdInternalId,
groupName, groupName,
forPhoneNumber: from, forPhoneNumber: from,
botNumber: row.phoneNumber, botNumber: row.phoneNumber,
response: createGroupResponse, response: createGroupResponse,
}); }, 'Created new Signal group');
} }
// Now handle notifications and message forwarding for both new and existing groups // Now handle notifications and message forwarding for both new and existing groups
if (finalTo && finalTo.startsWith("group.")) { if (finalTo && finalTo.startsWith("group.")) {
// Forward the user's initial message to the group using quote feature // Forward the user's initial message to the group using quote feature
try { try {
console.log( logger.debug('Forwarding initial message to group using quote feature');
`[receive-signal-message] Forwarding initial message to group using quote feature`,
);
const attributionMessage = `Message from ${from}:\n"${message}"\n\n---\nSupport team: Your request has been received. An agent will respond shortly.`; const attributionMessage = `Message from ${from}:\n"${message}"\n\n---\nSupport team: Your request has been received. An agent will respond shortly.`;
@ -183,21 +165,14 @@ const receiveSignalMessageTask = async ({
quoteTimestamp: Date.parse(sentAt), quoteTimestamp: Date.parse(sentAt),
}); });
console.log( logger.debug({ finalTo }, 'Successfully forwarded initial message to group');
`[receive-signal-message] Successfully forwarded initial message to group ${finalTo}`,
);
} catch (forwardError) { } catch (forwardError) {
console.error( logger.error({ error: forwardError }, 'Error forwarding message to group');
"[receive-signal-message] Error forwarding message to group:",
forwardError,
);
} }
// Send a response to the original DM informing about the group // Send a response to the original DM informing about the group
try { try {
console.log( logger.debug('Sending group notification to original DM');
`[receive-signal-message] Sending group notification to original DM`,
);
const dmNotification = `Hello! A private support group has been created for your conversation.\n\nGroup name: ${groupName}\n\nPlease look for the new group in your Signal app to continue the conversation. Our support team will respond there shortly.\n\nThank you for contacting support!`; const dmNotification = `Hello! A private support group has been created for your conversation.\n\nGroup name: ${groupName}\n\nPlease look for the new group in your Signal app to continue the conversation. Our support team will respond there shortly.\n\nThank you for contacting support!`;
@ -208,14 +183,9 @@ const receiveSignalMessageTask = async ({
conversationId: null, conversationId: null,
}); });
console.log( logger.debug('Successfully sent group notification to user DM');
`[receive-signal-message] Successfully sent group notification to user DM`,
);
} catch (dmError) { } catch (dmError) {
console.error( logger.error({ error: dmError }, 'Error sending DM notification');
"[receive-signal-message] Error sending DM notification:",
dmError,
);
} }
} }
} catch (error: any) { } catch (error: any) {
@ -227,16 +197,14 @@ const receiveSignalMessageTask = async ({
errorMessage?.toString().toLowerCase().includes("exists"); errorMessage?.toString().toLowerCase().includes("exists");
if (isAlreadyExists) { if (isAlreadyExists) {
console.log( logger.debug({ from }, 'Group might already exist, continuing with original recipient');
`[receive-signal-message] Group might already exist for ${from}, continuing with original recipient`,
);
} else { } else {
console.error("[receive-signal-message] Error creating Signal group:", { logger.error({
error: errorMessage, error: errorMessage,
from, from,
to, to,
botNumber: row.phoneNumber, botNumber: row.phoneNumber,
}); }, 'Error creating Signal group');
} }
} }
} }

View file

@ -1,7 +1,10 @@
import { db, getWorkerUtils } from "@link-stack/bridge-common"; import { db, getWorkerUtils } from "@link-stack/bridge-common";
import { createLogger } from "@link-stack/logger";
import * as signalApi from "@link-stack/signal-api"; import * as signalApi from "@link-stack/signal-api";
const { Configuration, MessagesApi, GroupsApi } = signalApi; const { Configuration, MessagesApi, GroupsApi } = signalApi;
const logger = createLogger('bridge-worker-send-signal-message');
interface SendSignalMessageTaskOptions { interface SendSignalMessageTaskOptions {
token: string; token: string;
to: string; to: string;
@ -21,12 +24,12 @@ const sendSignalMessageTask = async ({
quoteAuthor, quoteAuthor,
quoteTimestamp, quoteTimestamp,
}: SendSignalMessageTaskOptions): Promise<void> => { }: SendSignalMessageTaskOptions): Promise<void> => {
console.log(`[send-signal-message] Processing outgoing message:`, { logger.debug({
token, token,
to, to,
conversationId, conversationId,
messageLength: message?.length, messageLength: message?.length,
}); }, 'Processing outgoing message');
const bot = await db const bot = await db
.selectFrom("SignalBot") .selectFrom("SignalBot")
.selectAll() .selectAll()
@ -55,12 +58,12 @@ const sendSignalMessageTask = async ({
const isGroupId = isUUID || isGroupPrefix || isBase64; const isGroupId = isUUID || isGroupPrefix || isBase64;
const enableAutoGroups = process.env.BRIDGE_SIGNAL_AUTO_GROUPS === "true"; const enableAutoGroups = process.env.BRIDGE_SIGNAL_AUTO_GROUPS === "true";
console.log(`[send-signal-message] Recipient analysis:`, { logger.debug({
to, to,
isGroupId, isGroupId,
enableAutoGroups, enableAutoGroups,
shouldCreateGroup: enableAutoGroups && !isGroupId && to && conversationId, shouldCreateGroup: enableAutoGroups && !isGroupId && to && conversationId,
}); }, 'Recipient analysis');
// If sending to a phone number and auto-groups is enabled, create a group first // If sending to a phone number and auto-groups is enabled, create a group first
if (enableAutoGroups && !isGroupId && to && conversationId) { if (enableAutoGroups && !isGroupId && to && conversationId) {
@ -90,9 +93,7 @@ const sendSignalMessageTask = async ({
const createdGroup = groups.find((g) => g.id === finalTo); const createdGroup = groups.find((g) => g.id === finalTo);
if (createdGroup && createdGroup.internalId) { if (createdGroup && createdGroup.internalId) {
internalId = createdGroup.internalId; internalId = createdGroup.internalId;
console.log( logger.debug({ internalId }, 'Got actual internalId');
`[send-signal-message] Got actual internalId: ${internalId}`,
);
} else { } else {
// Fallback: extract base64 part from ID // Fallback: extract base64 part from ID
if (finalTo.startsWith("group.")) { if (finalTo.startsWith("group.")) {
@ -100,22 +101,20 @@ const sendSignalMessageTask = async ({
} }
} }
} catch (fetchError) { } catch (fetchError) {
console.log( logger.debug('Could not fetch group details, using ID base64 part');
`[send-signal-message] Could not fetch group details, using ID base64 part`,
);
// Fallback: extract base64 part from ID // Fallback: extract base64 part from ID
if (finalTo.startsWith("group.")) { if (finalTo.startsWith("group.")) {
internalId = finalTo.substring(6); internalId = finalTo.substring(6);
} }
} }
console.log(`[send-signal-message] Created new Signal group:`, { logger.debug({
groupId: finalTo, groupId: finalTo,
internalId, internalId,
groupName, groupName,
conversationId, conversationId,
originalRecipient: to, originalRecipient: to,
botNumber: bot.phoneNumber, botNumber: bot.phoneNumber,
}); }, 'Created new Signal group');
// Notify Zammad about the new group ID via webhook // Notify Zammad about the new group ID via webhook
await worker.addJob("common/notify-webhooks", { await worker.addJob("common/notify-webhooks", {
@ -131,23 +130,23 @@ const sendSignalMessageTask = async ({
}); });
} }
} catch (groupError) { } catch (groupError) {
console.error("[send-signal-message] Error creating Signal group:", { logger.error({
error: groupError instanceof Error ? groupError.message : groupError, error: groupError instanceof Error ? groupError.message : groupError,
to, to,
conversationId, conversationId,
}); }, 'Error creating Signal group');
// Continue with original recipient if group creation fails // Continue with original recipient if group creation fails
} }
} }
console.log(`[send-signal-message] Sending message via API:`, { logger.debug({
fromNumber: number, fromNumber: number,
toRecipient: finalTo, toRecipient: finalTo,
originalTo: to, originalTo: to,
recipientChanged: to !== finalTo, recipientChanged: to !== finalTo,
groupCreated, groupCreated,
isGroupRecipient: finalTo.startsWith("group."), isGroupRecipient: finalTo.startsWith("group."),
}); }, 'Sending message via API');
// Build the message data with optional quote parameters // Build the message data with optional quote parameters
const messageData: signalApi.ApiSendMessageV2 = { const messageData: signalApi.ApiSendMessageV2 = {
@ -156,12 +155,12 @@ const sendSignalMessageTask = async ({
message, message,
}; };
console.log(`[send-signal-message] Message data being sent:`, { logger.debug({
number, number,
recipients: [finalTo], recipients: [finalTo],
message: message.substring(0, 50) + "...", message: message.substring(0, 50) + "...",
hasQuoteParams: !!(quoteMessage && quoteAuthor && quoteTimestamp), hasQuoteParams: !!(quoteMessage && quoteAuthor && quoteTimestamp),
}); }, 'Message data being sent');
// Add quote parameters if all are provided // Add quote parameters if all are provided
if (quoteMessage && quoteAuthor && quoteTimestamp) { if (quoteMessage && quoteAuthor && quoteTimestamp) {
@ -169,28 +168,28 @@ const sendSignalMessageTask = async ({
messageData.quoteAuthor = quoteAuthor; messageData.quoteAuthor = quoteAuthor;
messageData.quoteMessage = quoteMessage; messageData.quoteMessage = quoteMessage;
console.log(`[send-signal-message] Including quote in message:`, { logger.debug({
quoteAuthor, quoteAuthor,
quoteMessage: quoteMessage.substring(0, 50) + "...", quoteMessage: quoteMessage.substring(0, 50) + "...",
quoteTimestamp, quoteTimestamp,
}); }, 'Including quote in message');
} }
const response = await messagesClient.v2SendPost({ const response = await messagesClient.v2SendPost({
data: messageData, data: messageData,
}); });
console.log(`[send-signal-message] Message sent successfully:`, { logger.debug({
to: finalTo, to: finalTo,
groupCreated, groupCreated,
response: response?.timestamp || "no timestamp", response: response?.timestamp || "no timestamp",
}); }, 'Message sent successfully');
} catch (error: any) { } catch (error: any) {
// Try to get the actual error message from the response // Try to get the actual error message from the response
if (error.response) { if (error.response) {
try { try {
const errorBody = await error.response.text(); const errorBody = await error.response.text();
console.error(`[send-signal-message] Signal API error:`, { logger.error({
status: error.response.status, status: error.response.status,
statusText: error.response.statusText, statusText: error.response.statusText,
body: errorBody, body: errorBody,
@ -200,12 +199,12 @@ const sendSignalMessageTask = async ({
toRecipients: [finalTo], toRecipients: [finalTo],
hasQuote: !!quoteMessage, hasQuote: !!quoteMessage,
}, },
}); }, 'Signal API error');
} catch (e) { } catch (e) {
console.error(`[send-signal-message] Could not parse error response`); logger.error('Could not parse error response');
} }
} }
console.error(`[send-signal-message] Full error:`, error); logger.error({ error }, 'Full error details');
throw error; throw error;
} }
}; };

View file

@ -3,6 +3,9 @@ import { withDb, AppDatabase } from "../../lib/db.js";
import { twilioClientFor } from "../../lib/common.js"; import { twilioClientFor } from "../../lib/common.js";
import { CallInstance } from "twilio/lib/rest/api/v2010/account/call"; import { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
import workerUtils from "../../lib/utils.js"; import workerUtils from "../../lib/utils.js";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('bridge-worker-twilio-recording');
interface WebhookPayload { interface WebhookPayload {
startTime: string; startTime: string;
@ -20,7 +23,7 @@ const getTwilioRecording = async (url: string) => {
const { payload } = await Wreck.get(url); const { payload } = await Wreck.get(url);
return { recording: payload as Buffer }; return { recording: payload as Buffer };
} catch (error: any) { } catch (error: any) {
console.error(error.output); logger.error(error.output);
return { error: error.output }; return { error: error.output };
} }
}; };

View file

@ -1,4 +1,7 @@
import { db } from "@link-stack/bridge-common"; import { db } from "@link-stack/bridge-common";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('bridge-worker-send-whatsapp-message');
interface SendWhatsappMessageTaskOptions { interface SendWhatsappMessageTaskOptions {
token: string; token: string;
@ -26,7 +29,7 @@ const sendWhatsappMessageTask = async ({
body: JSON.stringify(params), body: JSON.stringify(params),
}); });
} catch (error) { } catch (error) {
console.error({ error }); logger.error({ error });
throw new Error("Failed to send message"); throw new Error("Failed to send message");
} }
}; };

View file

@ -3,6 +3,9 @@ import Google from "next-auth/providers/google";
import Apple from "next-auth/providers/apple"; import Apple from "next-auth/providers/apple";
import Credentials from "next-auth/providers/credentials"; import Credentials from "next-auth/providers/credentials";
import { checkAuth } from "./opensearch"; import { checkAuth } from "./opensearch";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('leafcutter-auth');
export const authOptions: NextAuthOptions = { export const authOptions: NextAuthOptions = {
pages: { pages: {
@ -45,7 +48,7 @@ export const authOptions: NextAuthOptions = {
return user; return user;
} catch (e) { } catch (e) {
console.error({ e }); logger.error({ e });
} }
return null; return null;

View file

@ -1,6 +1,9 @@
/* eslint-disable no-underscore-dangle */ /* eslint-disable no-underscore-dangle */
import { Client } from "@opensearch-project/opensearch"; import { Client } from "@opensearch-project/opensearch";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('leafcutter-opensearch');
/* Common */ /* Common */
@ -302,7 +305,7 @@ export const updateUserVisualization = async (
body, body,
}); });
} catch (e) { } catch (e) {
console.error({ e }); logger.error({ e });
} }
return id; return id;

View file

@ -4,6 +4,9 @@ import { Client } from "@opensearch-project/opensearch";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import taxonomy from "app/_config/taxonomy.json"; import taxonomy from "app/_config/taxonomy.json";
import unRegions from "app/_config/unRegions.json"; import unRegions from "app/_config/unRegions.json";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('leafcutter-index');
export const POST = async (req: NextRequest) => { export const POST = async (req: NextRequest) => {
const { tickets } = await req.json(); const { tickets } = await req.json();
@ -46,7 +49,7 @@ export const POST = async (req: NextRequest) => {
}); });
succeeded.push(id); succeeded.push(id);
} catch (e) { } catch (e) {
console.error(e); logger.error(e);
failed.push(id); failed.push(id);
} }
} }

View file

@ -18,6 +18,7 @@
"@emotion/server": "^11.11.0", "@emotion/server": "^11.11.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@link-stack/leafcutter-ui": "*", "@link-stack/leafcutter-ui": "*",
"@link-stack/logger": "*",
"@link-stack/opensearch-common": "*", "@link-stack/opensearch-common": "*",
"@mui/icons-material": "^6", "@mui/icons-material": "^6",
"@mui/material": "^6", "@mui/material": "^6",

View file

@ -1,6 +1,9 @@
import { createProxyMiddleware } from "http-proxy-middleware"; import { createProxyMiddleware } from "http-proxy-middleware";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt"; import { getToken } from "next-auth/jwt";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('leafcutter-[[...path]]');
/* /*
@ -30,11 +33,11 @@ const withAuthInfo =
); );
if (requestSignature && isAppPath) { if (requestSignature && isAppPath) {
console.info("Has Signature"); logger.info("Has Signature");
} }
if (referrerSignature && isResourcePath) { if (referrerSignature && isResourcePath) {
console.info("Has Signature"); logger.info("Has Signature");
} }
if (!email) { if (!email) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,6 +12,9 @@ import Credentials from "next-auth/providers/credentials";
import Apple from "next-auth/providers/apple"; import Apple from "next-auth/providers/apple";
import { Redis } from "ioredis"; import { Redis } from "ioredis";
import AzureADProvider from "next-auth/providers/azure-ad"; 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}` }; 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); return roles.filter((role: string) => role !== null);
} catch (e) { } catch (e) {
console.error({ e }); logger.error({ error: e, email }, 'Failed to get user roles');
return []; 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) => { 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(); const json = await res.json();
return json; return json;
} catch (error) { } catch (error) {
console.error({ error }); logger.error({ error }, "Error occurred");
return null; return null;
} }
}; };

View file

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

View file

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

5680
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -62,21 +62,10 @@
"typescript": "latest" "typescript": "latest"
}, },
"overrides": { "overrides": {
"material-ui-popup-state": { "react": "^19.0.0",
"react": "$react" "react-dom": "^19.0.0",
}, "@types/react": "^19.0.12",
"@chatscope/chat-ui-kit-react": { "@mui/material": "^6.5.0"
"react": "$react",
"react-dom": "$react-dom"
},
"@mui/icons-material": {
"react": "$react"
},
"mui-chips-input": {
"react": "$react",
"react-dom": "$react-dom",
"@types/react": "$@types/react"
}
}, },
"engines": { "engines": {
"npm": ">=10", "npm": ">=10",

View file

@ -0,0 +1,36 @@
{
"name": "@link-stack/logger",
"version": "1.0.0",
"description": "Shared logging utility for Link Stack monorepo",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"lint": "eslint . --ext .ts",
"type-check": "tsc --noEmit"
},
"dependencies": {
"pino": "^9.5.0",
"pino-pretty": "^13.0.0"
},
"devDependencies": {
"@link-stack/eslint-config": "*",
"@link-stack/typescript-config": "*",
"@types/node": "^22.10.5",
"eslint": "^9.17.0",
"tsup": "^8.3.5",
"typescript": "^5.7.3"
},
"publishConfig": {
"access": "public"
}
}

View file

@ -0,0 +1,56 @@
import type { LoggerOptions } from 'pino';
export const getLogLevel = (): string => {
return process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug');
};
export const getPinoConfig = (): LoggerOptions => {
const isDevelopment = process.env.NODE_ENV !== 'production';
const baseConfig: LoggerOptions = {
level: getLogLevel(),
formatters: {
level: (label) => {
return { level: label.toUpperCase() };
},
},
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
redact: {
paths: [
'password',
'token',
'secret',
'api_key',
'apiKey',
'authorization',
'cookie',
'*.password',
'*.token',
'*.secret',
'*.api_key',
'*.apiKey',
],
censor: '[REDACTED]',
},
};
if (isDevelopment) {
// In development, use pretty printing for better readability
return {
...baseConfig,
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
singleLine: false,
messageFormat: '{msg}',
},
},
};
}
// In production, use JSON for structured logging
return baseConfig;
};

View file

@ -0,0 +1,35 @@
import pino, { Logger as PinoLogger } from 'pino';
import { getPinoConfig } from './config';
export type Logger = PinoLogger;
// Create the default logger instance
export const logger: Logger = pino(getPinoConfig());
// Factory function to create child loggers with context
export const createLogger = (name: string, context?: Record<string, any>): Logger => {
return logger.child({ name, ...context });
};
// Export log levels for consistency
export const LogLevel = {
TRACE: 'trace',
DEBUG: 'debug',
INFO: 'info',
WARN: 'warn',
ERROR: 'error',
FATAL: 'fatal',
} as const;
export type LogLevelType = typeof LogLevel[keyof typeof LogLevel];
// Utility to check if a log level is enabled
export const isLogLevelEnabled = (level: LogLevelType): boolean => {
return logger.isLevelEnabled(level);
};
// Re-export pino types that might be useful
export type { LoggerOptions, DestinationStream } from 'pino';
// Default export for convenience
export default logger;

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es2022",
"lib": ["es2022"],
"module": "esnext",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowJs": false,
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View file

@ -1,6 +1,9 @@
/* eslint-disable no-underscore-dangle */ /* eslint-disable no-underscore-dangle */
import { Client } from "@opensearch-project/opensearch"; import { Client } from "@opensearch-project/opensearch";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('opensearch-common-opensearch');
/* Common */ /* Common */
@ -284,7 +287,7 @@ export const updateUserVisualization = async (
}); });
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error({ e }); logger.error({ e });
} }
return id; return id;

View file

@ -5,6 +5,7 @@
"build": "tsc -p tsconfig.json" "build": "tsc -p tsconfig.json"
}, },
"dependencies": { "dependencies": {
"@link-stack/logger": "*",
"@opensearch-project/opensearch": "^3.4.0", "@opensearch-project/opensearch": "^3.4.0",
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },

View file

@ -4,9 +4,12 @@ import { promises as fs } from "fs";
import { glob } from "glob"; import { glob } from "glob";
import path from "path"; import path from "path";
import os from "os"; import os from "os";
import { createLogger } from "@link-stack/logger";
const logger = createLogger('zammad-addon-build');
const packageFile = async (actualPath: string): Promise<any> => { const packageFile = async (actualPath: string): Promise<any> => {
console.info(`Packaging: ${actualPath}`); logger.info({ actualPath }, 'Packaging file');
const packagePath = actualPath.slice(4); const packagePath = actualPath.slice(4);
const data = await fs.readFile(actualPath, "utf-8"); const data = await fs.readFile(actualPath, "utf-8");
const content = Buffer.from(data, "utf-8").toString("base64"); const content = Buffer.from(data, "utf-8").toString("base64");
@ -74,10 +77,10 @@ export const createZPM = async ({
for (const file of files) { for (const file of files) {
await fs.unlink(file); await fs.unlink(file);
console.info(`${file} was deleted`); logger.info({ file }, 'File was deleted');
} }
} catch (err) { } catch (err) {
console.error(err); logger.error({ error: err }, 'Error removing old addon files');
} }
await fs.writeFile( await fs.writeFile(
`../../docker/zammad/addons/${name}-v${version}.zpm`, `../../docker/zammad/addons/${name}-v${version}.zpm`,
@ -89,7 +92,7 @@ export const createZPM = async ({
const main = async () => { const main = async () => {
const packageJSON = JSON.parse(await fs.readFile("./package.json", "utf-8")); const packageJSON = JSON.parse(await fs.readFile("./package.json", "utf-8"));
const { name: fullName, displayName, version } = packageJSON; const { name: fullName, displayName, version } = packageJSON;
console.info(`Building addon ${displayName} v${version}`); logger.info({ displayName, version }, 'Building addon');
const name = fullName.split("/").pop(); const name = fullName.split("/").pop();
await createZPM({ name, displayName, version }); await createZPM({ name, displayName, version });
}; };

View file

@ -16,6 +16,7 @@
"author": "", "author": "",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@link-stack/logger": "*",
"glob": "^11.0.1" "glob": "^11.0.1"
} }
} }