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:
parent
5b89bfce7c
commit
c1feaa4cb1
42 changed files with 3824 additions and 2422 deletions
|
|
@ -10,6 +10,9 @@ import {
|
|||
CamelCasePlugin,
|
||||
} from "kysely";
|
||||
import pkg from "pg";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
|
||||
const logger = createLogger('bridge-migrations-migrate');
|
||||
const { Pool } = pkg;
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
|
|
@ -72,17 +75,17 @@ export const migrate = async (arg: string) => {
|
|||
|
||||
results?.forEach((it) => {
|
||||
if (it.status === "Success") {
|
||||
console.info(
|
||||
logger.info(
|
||||
`Migration "${it.migrationName} ${it.direction.toLowerCase()}" was executed successfully`,
|
||||
);
|
||||
} else if (it.status === "Error") {
|
||||
console.error(`Failed to execute migration "${it.migrationName}"`);
|
||||
logger.error(`Failed to execute migration "${it.migrationName}"`);
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Failed to migrate");
|
||||
console.error(error);
|
||||
logger.error("Failed to migrate");
|
||||
logger.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"migrate:down:one": "tsx migrate.ts down:one"
|
||||
},
|
||||
"dependencies": {
|
||||
"@link-stack/logger": "*",
|
||||
"dotenv": "^16.4.7",
|
||||
"kysely": "0.27.6",
|
||||
"pg": "^8.14.1",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import {
|
|||
SendMessageRoute,
|
||||
ReceiveMessageRoute,
|
||||
} from "./routes.js";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
|
||||
const logger = createLogger('bridge-whatsapp-index');
|
||||
|
||||
const server = Hapi.server({ port: 5000 });
|
||||
|
||||
|
|
@ -34,6 +37,6 @@ const main = async () => {
|
|||
};
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import makeWASocket, {
|
|||
useMultiFileAuthState,
|
||||
} from "@whiskeysockets/baileys";
|
||||
import fs from "fs";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
|
||||
const logger = createLogger('bridge-whatsapp-service');
|
||||
|
||||
export type AuthCompleteCallback = (error?: string) => void;
|
||||
|
||||
|
|
@ -57,7 +60,7 @@ export default class WhatsappService extends Service {
|
|||
try {
|
||||
connection.end(null);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.error({ error }, 'Connection reset error');
|
||||
}
|
||||
}
|
||||
this.connections = {};
|
||||
|
|
@ -92,27 +95,27 @@ export default class WhatsappService extends Service {
|
|||
isNewLogin,
|
||||
} = update;
|
||||
if (qr) {
|
||||
console.info("got qr code");
|
||||
logger.info('got qr code');
|
||||
const botDirectory = this.getBotDirectory(botID);
|
||||
const qrPath = `${botDirectory}/qr.txt`;
|
||||
fs.writeFileSync(qrPath, qr, "utf8");
|
||||
} else if (isNewLogin) {
|
||||
console.info("got new login");
|
||||
logger.info('got new login');
|
||||
const botDirectory = this.getBotDirectory(botID);
|
||||
const verifiedFile = `${botDirectory}/verified`;
|
||||
fs.writeFileSync(verifiedFile, "");
|
||||
} else if (connectionState === "open") {
|
||||
console.info("opened connection");
|
||||
logger.info('opened connection');
|
||||
} else if (connectionState === "close") {
|
||||
console.info("connection closed due to ", lastDisconnect?.error);
|
||||
logger.info({ lastDisconnect }, 'connection closed');
|
||||
const disconnectStatusCode = (lastDisconnect?.error as any)?.output
|
||||
?.statusCode;
|
||||
if (disconnectStatusCode === DisconnectReason.restartRequired) {
|
||||
console.info("reconnecting after got new login");
|
||||
logger.info('reconnecting after got new login');
|
||||
await this.createConnection(botID, server, options);
|
||||
authCompleteCallback?.();
|
||||
} else if (disconnectStatusCode !== DisconnectReason.loggedOut) {
|
||||
console.info("reconnecting");
|
||||
logger.info('reconnecting');
|
||||
await this.sleep(pause);
|
||||
pause *= 2;
|
||||
this.createConnection(botID, server, options);
|
||||
|
|
@ -121,12 +124,12 @@ export default class WhatsappService extends Service {
|
|||
}
|
||||
|
||||
if (events["creds.update"]) {
|
||||
console.info("creds update");
|
||||
logger.info('creds update');
|
||||
await saveCreds();
|
||||
}
|
||||
|
||||
if (events["messages.upsert"]) {
|
||||
console.info("messages upsert");
|
||||
logger.info('messages upsert');
|
||||
const upsert = events["messages.upsert"];
|
||||
const { messages } = upsert;
|
||||
if (messages) {
|
||||
|
|
@ -149,7 +152,7 @@ export default class WhatsappService extends Service {
|
|||
const verifiedFile = `${directory}/verified`;
|
||||
if (fs.existsSync(verifiedFile)) {
|
||||
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, {
|
||||
browser: WhatsappService.browserDescription,
|
||||
|
|
@ -169,9 +172,9 @@ export default class WhatsappService extends Service {
|
|||
message,
|
||||
messageTimestamp,
|
||||
} = webMessageInfo;
|
||||
console.info("Message type debug");
|
||||
logger.info('Message type debug');
|
||||
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 =
|
||||
message && remoteJid !== "status@broadcast" && !fromMe;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { run } from "graphile-worker";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
import * as path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const logger = createLogger('bridge-worker');
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const startWorker = async () => {
|
||||
console.info("Starting worker...");
|
||||
logger.info("Starting worker...");
|
||||
|
||||
await run({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
|
|
@ -30,6 +32,6 @@ const main = async () => {
|
|||
};
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
logger.error({ error: err }, 'Worker failed to start');
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
import Twilio from "twilio";
|
||||
import { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
|
||||
import { Zammad, getOrCreateUser } from "./zammad.js";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
|
||||
const logger = createLogger('bridge-worker-common');
|
||||
|
||||
type SavedVoiceProvider = any;
|
||||
|
||||
|
|
@ -63,7 +66,7 @@ export const createZammadTicket = async (
|
|||
});
|
||||
} catch (error: any) {
|
||||
if (error.isBoom) {
|
||||
console.error(error.output);
|
||||
logger.error({ output: error.output }, 'Zammad ticket creation failed');
|
||||
throw new Error("Failed to create zamamd ticket");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
import { Readable } from "stream";
|
||||
import ffmpeg from "fluent-ffmpeg";
|
||||
import * as R from "remeda";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
|
||||
const logger = createLogger('bridge-worker-media-convert');
|
||||
|
||||
const requiredCodecs = ["mp3", "webm", "wav"];
|
||||
|
||||
|
|
@ -36,7 +39,7 @@ export const convert = (
|
|||
.audioBitrate(settings.bitrate)
|
||||
.toFormat(settings.format)
|
||||
.on("error", (err, _stdout, _stderr) => {
|
||||
console.error(err.message);
|
||||
logger.error({ error: err }, 'FFmpeg conversion error');
|
||||
reject(err);
|
||||
})
|
||||
.on("end", () => {
|
||||
|
|
@ -58,7 +61,7 @@ export const selfCheck = (): Promise<boolean> => {
|
|||
return new Promise((resolve) => {
|
||||
ffmpeg.getAvailableFormats((err, codecs) => {
|
||||
if (err) {
|
||||
console.error("FFMPEG error:", err);
|
||||
logger.error({ error: err }, 'FFMPEG error');
|
||||
resolve(false);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"dependencies": {
|
||||
"@hapi/wreck": "^18.1.0",
|
||||
"@link-stack/bridge-common": "*",
|
||||
"@link-stack/logger": "*",
|
||||
"@link-stack/signal-api": "*",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"graphile-worker": "^0.16.6",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { db } from "@link-stack/bridge-common";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
|
||||
const logger = createLogger('notify-webhooks');
|
||||
|
||||
export interface NotifyWebhooksOptions {
|
||||
backendId: string;
|
||||
|
|
@ -10,11 +13,10 @@ const notifyWebhooksTask = async (
|
|||
): Promise<void> => {
|
||||
const { backendId, payload } = options;
|
||||
|
||||
console.log(`[notify-webhooks] Processing webhook notification:`, {
|
||||
logger.debug({
|
||||
backendId,
|
||||
payloadKeys: Object.keys(payload),
|
||||
payload: JSON.stringify(payload, null, 2),
|
||||
});
|
||||
}, 'Processing webhook notification');
|
||||
|
||||
const webhooks = await db
|
||||
.selectFrom("Webhook")
|
||||
|
|
@ -22,20 +24,19 @@ const notifyWebhooksTask = async (
|
|||
.where("backendId", "=", backendId)
|
||||
.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) {
|
||||
const { endpointUrl, httpMethod, headers } = webhook;
|
||||
const finalHeaders = { "Content-Type": "application/json", ...headers };
|
||||
const body = JSON.stringify(payload);
|
||||
|
||||
console.log(`[notify-webhooks] Sending webhook:`, {
|
||||
logger.debug({
|
||||
url: endpointUrl,
|
||||
method: httpMethod,
|
||||
bodyLength: body.length,
|
||||
headers: Object.keys(finalHeaders),
|
||||
payload: body,
|
||||
});
|
||||
headerKeys: Object.keys(finalHeaders),
|
||||
}, 'Sending webhook');
|
||||
|
||||
try {
|
||||
const result = await fetch(endpointUrl, {
|
||||
|
|
@ -44,26 +45,26 @@ const notifyWebhooksTask = async (
|
|||
body,
|
||||
});
|
||||
|
||||
console.log(`[notify-webhooks] Webhook response:`, {
|
||||
logger.debug({
|
||||
url: endpointUrl,
|
||||
status: result.status,
|
||||
statusText: result.statusText,
|
||||
ok: result.ok,
|
||||
});
|
||||
}, 'Webhook response');
|
||||
|
||||
if (!result.ok) {
|
||||
const responseText = await result.text();
|
||||
console.error(`[notify-webhooks] Webhook error response:`, {
|
||||
logger.error({
|
||||
url: endpointUrl,
|
||||
status: result.status,
|
||||
response: responseText.substring(0, 500), // First 500 chars
|
||||
});
|
||||
responseSample: responseText.substring(0, 500),
|
||||
}, 'Webhook error response');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[notify-webhooks] Webhook request failed:`, {
|
||||
logger.error({
|
||||
url: endpointUrl,
|
||||
error: error instanceof Error ? error.message : error,
|
||||
});
|
||||
}, 'Webhook request failed');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { db } from "@link-stack/bridge-common";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
|
||||
const logger = createLogger('bridge-worker-send-facebook-message');
|
||||
|
||||
interface SendFacebookMessageTaskOptions {
|
||||
token: string;
|
||||
|
|
@ -32,7 +35,7 @@ const sendFacebookMessageTask = async (
|
|||
body: JSON.stringify(outgoingMessage),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error({ error });
|
||||
logger.error({ error });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { db, getWorkerUtils } from "@link-stack/bridge-common";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
import * as signalApi from "@link-stack/signal-api";
|
||||
|
||||
const logger = createLogger('fetch-signal-messages');
|
||||
|
||||
const { Configuration, MessagesApi, AttachmentsApi } = signalApi;
|
||||
const config = new Configuration({
|
||||
basePath: process.env.BRIDGE_SIGNAL_URL,
|
||||
|
|
@ -59,8 +62,7 @@ const processMessage = async ({
|
|||
const { attachments } = dataMessage;
|
||||
const rawTimestamp = dataMessage?.timestamp;
|
||||
|
||||
// Debug logging for group detection
|
||||
console.log(`[fetch-signal-messages] Processing message:`, {
|
||||
logger.debug({
|
||||
sourceUuid,
|
||||
source,
|
||||
rawTimestamp,
|
||||
|
|
@ -71,7 +73,7 @@ const processMessage = async ({
|
|||
groupV2Id: dataMessage?.groupV2?.id,
|
||||
groupContextType: dataMessage?.groupContext?.type,
|
||||
groupInfoType: dataMessage?.groupInfo?.type,
|
||||
});
|
||||
}, 'Processing message');
|
||||
const timestamp = new Date(rawTimestamp);
|
||||
|
||||
const formattedAttachments = await fetchAttachments(attachments);
|
||||
|
|
@ -148,9 +150,7 @@ const fetchSignalMessagesTask = async ({
|
|||
number: phoneNumber,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[fetch-signal-messages] Fetching messages for bot ${id} (${phoneNumber})`,
|
||||
);
|
||||
logger.debug({ botId: id, phoneNumber }, 'Fetching messages for bot');
|
||||
|
||||
for (const message of messages) {
|
||||
const formattedMessages = await processMessage({
|
||||
|
|
@ -160,14 +160,14 @@ const fetchSignalMessagesTask = async ({
|
|||
});
|
||||
for (const formattedMessage of formattedMessages) {
|
||||
if (formattedMessage.to !== formattedMessage.from) {
|
||||
console.log(`[fetch-signal-messages] Creating job for message:`, {
|
||||
logger.debug({
|
||||
messageId: formattedMessage.messageId,
|
||||
from: formattedMessage.from,
|
||||
to: formattedMessage.to,
|
||||
isGroup: formattedMessage.isGroup,
|
||||
hasMessage: !!formattedMessage.message,
|
||||
hasAttachment: !!formattedMessage.attachment,
|
||||
});
|
||||
}, 'Creating job for message');
|
||||
|
||||
await worker.addJob(
|
||||
"signal/receive-signal-message",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
/*
|
||||
import { URLSearchParams } from "url";
|
||||
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";
|
||||
|
||||
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, {
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { db, getWorkerUtils } from "@link-stack/bridge-common";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
import * as signalApi from "@link-stack/signal-api";
|
||||
const { Configuration, GroupsApi } = signalApi;
|
||||
|
||||
const logger = createLogger('bridge-worker-receive-signal-message');
|
||||
|
||||
interface ReceiveSignalMessageTaskOptions {
|
||||
token: string;
|
||||
to: string;
|
||||
|
|
@ -27,7 +30,7 @@ const receiveSignalMessageTask = async ({
|
|||
mimeType,
|
||||
isGroup,
|
||||
}: ReceiveSignalMessageTaskOptions): Promise<void> => {
|
||||
console.log(`[receive-signal-message] Processing incoming message:`, {
|
||||
logger.debug({
|
||||
messageId,
|
||||
from,
|
||||
to,
|
||||
|
|
@ -35,7 +38,7 @@ const receiveSignalMessageTask = async ({
|
|||
hasMessage: !!message,
|
||||
hasAttachment: !!attachment,
|
||||
token,
|
||||
});
|
||||
}, 'Processing incoming message');
|
||||
const worker = await getWorkerUtils();
|
||||
const row = await db
|
||||
.selectFrom("SignalBot")
|
||||
|
|
@ -50,20 +53,18 @@ const receiveSignalMessageTask = async ({
|
|||
// Check if auto-group creation is enabled and this is NOT already a group message
|
||||
const enableAutoGroups = process.env.BRIDGE_SIGNAL_AUTO_GROUPS === "true";
|
||||
|
||||
console.log(`[receive-signal-message] Auto-groups config:`, {
|
||||
logger.debug({
|
||||
enableAutoGroups,
|
||||
isGroup,
|
||||
shouldCreateGroup: enableAutoGroups && !isGroup && from && to,
|
||||
});
|
||||
}, 'Auto-groups config');
|
||||
|
||||
// If this is already a group message and auto-groups is enabled,
|
||||
// use group provided in 'to'
|
||||
if (enableAutoGroups && isGroup && to) {
|
||||
// Signal sends the internal ID (base64) in group messages
|
||||
// We should NOT add "group." prefix - that's for sending messages, not receiving
|
||||
console.log(
|
||||
`[receive-signal-message] Message is from existing group with internal ID`,
|
||||
);
|
||||
logger.debug('Message is from existing group with internal ID');
|
||||
|
||||
finalTo = 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
|
||||
// This ensures each conversation gets its own group/ticket
|
||||
console.log(
|
||||
`[receive-signal-message] Creating new group for user ${from}`,
|
||||
);
|
||||
logger.info({ from }, 'Creating new group for user');
|
||||
|
||||
// Include timestamp to make each group unique
|
||||
const timestamp = new Date()
|
||||
|
|
@ -96,10 +95,7 @@ const receiveSignalMessageTask = async ({
|
|||
},
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[receive-signal-message] Full group creation response from Signal API:`,
|
||||
JSON.stringify(createGroupResponse, null, 2),
|
||||
);
|
||||
logger.debug({ createGroupResponse }, 'Group creation response from Signal API');
|
||||
|
||||
if (createGroupResponse.id) {
|
||||
// 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
|
||||
// The base64 part of the ID is NOT the same as the internalId!
|
||||
try {
|
||||
console.log(
|
||||
`[receive-signal-message] Fetching group details to get internalId...`,
|
||||
);
|
||||
logger.debug('Fetching group details to get internalId');
|
||||
const groups = await groupsClient.v1GroupsNumberGet({
|
||||
number: row.phoneNumber,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[receive-signal-message] All groups for bot:`,
|
||||
JSON.stringify(groups.slice(0, 3), null, 2), // Show first 3 to avoid too much output
|
||||
);
|
||||
logger.debug({ groupsSample: groups.slice(0, 3) }, 'Groups for bot');
|
||||
|
||||
const createdGroup = groups.find((g) => g.id === finalTo);
|
||||
if (createdGroup) {
|
||||
console.log(
|
||||
`[receive-signal-message] Found created group details:`,
|
||||
JSON.stringify(createdGroup, null, 2),
|
||||
);
|
||||
logger.debug({ createdGroup }, 'Found created group details');
|
||||
}
|
||||
|
||||
if (createdGroup && createdGroup.internalId) {
|
||||
createdInternalId = createdGroup.internalId;
|
||||
console.log(
|
||||
`[receive-signal-message] Got actual internalId: ${createdInternalId}`,
|
||||
);
|
||||
logger.debug({ createdInternalId }, 'Got actual internalId');
|
||||
} else {
|
||||
// Fallback: extract base64 part from ID
|
||||
if (finalTo.startsWith("group.")) {
|
||||
|
|
@ -140,36 +126,32 @@ const receiveSignalMessageTask = async ({
|
|||
}
|
||||
}
|
||||
} catch (fetchError) {
|
||||
console.log(
|
||||
`[receive-signal-message] Could not fetch group details, using ID base64 part`,
|
||||
);
|
||||
logger.debug('Could not fetch group details, using ID base64 part');
|
||||
// Fallback: extract base64 part from ID
|
||||
if (finalTo.startsWith("group.")) {
|
||||
createdInternalId = finalTo.substring(6);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[receive-signal-message] Group created successfully:`, {
|
||||
logger.debug({
|
||||
fullGroupId: finalTo,
|
||||
internalId: createdInternalId,
|
||||
});
|
||||
console.log(`[receive-signal-message] Created new Signal group:`, {
|
||||
}, 'Group created successfully');
|
||||
logger.debug({
|
||||
groupId: finalTo,
|
||||
internalId: createdInternalId,
|
||||
groupName,
|
||||
forPhoneNumber: from,
|
||||
botNumber: row.phoneNumber,
|
||||
response: createGroupResponse,
|
||||
});
|
||||
}, 'Created new Signal group');
|
||||
}
|
||||
|
||||
// Now handle notifications and message forwarding for both new and existing groups
|
||||
if (finalTo && finalTo.startsWith("group.")) {
|
||||
// Forward the user's initial message to the group using quote feature
|
||||
try {
|
||||
console.log(
|
||||
`[receive-signal-message] Forwarding initial message to group using quote feature`,
|
||||
);
|
||||
logger.debug('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.`;
|
||||
|
||||
|
|
@ -183,21 +165,14 @@ const receiveSignalMessageTask = async ({
|
|||
quoteTimestamp: Date.parse(sentAt),
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[receive-signal-message] Successfully forwarded initial message to group ${finalTo}`,
|
||||
);
|
||||
logger.debug({ finalTo }, 'Successfully forwarded initial message to group');
|
||||
} catch (forwardError) {
|
||||
console.error(
|
||||
"[receive-signal-message] Error forwarding message to group:",
|
||||
forwardError,
|
||||
);
|
||||
logger.error({ error: forwardError }, 'Error forwarding message to group');
|
||||
}
|
||||
|
||||
// Send a response to the original DM informing about the group
|
||||
try {
|
||||
console.log(
|
||||
`[receive-signal-message] Sending group notification to original DM`,
|
||||
);
|
||||
logger.debug('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!`;
|
||||
|
||||
|
|
@ -208,14 +183,9 @@ const receiveSignalMessageTask = async ({
|
|||
conversationId: null,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[receive-signal-message] Successfully sent group notification to user DM`,
|
||||
);
|
||||
logger.debug('Successfully sent group notification to user DM');
|
||||
} catch (dmError) {
|
||||
console.error(
|
||||
"[receive-signal-message] Error sending DM notification:",
|
||||
dmError,
|
||||
);
|
||||
logger.error({ error: dmError }, 'Error sending DM notification');
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
|
@ -227,16 +197,14 @@ const receiveSignalMessageTask = async ({
|
|||
errorMessage?.toString().toLowerCase().includes("exists");
|
||||
|
||||
if (isAlreadyExists) {
|
||||
console.log(
|
||||
`[receive-signal-message] Group might already exist for ${from}, continuing with original recipient`,
|
||||
);
|
||||
logger.debug({ from }, 'Group might already exist, continuing with original recipient');
|
||||
} else {
|
||||
console.error("[receive-signal-message] Error creating Signal group:", {
|
||||
logger.error({
|
||||
error: errorMessage,
|
||||
from,
|
||||
to,
|
||||
botNumber: row.phoneNumber,
|
||||
});
|
||||
}, 'Error creating Signal group');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { db, getWorkerUtils } from "@link-stack/bridge-common";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
import * as signalApi from "@link-stack/signal-api";
|
||||
const { Configuration, MessagesApi, GroupsApi } = signalApi;
|
||||
|
||||
const logger = createLogger('bridge-worker-send-signal-message');
|
||||
|
||||
interface SendSignalMessageTaskOptions {
|
||||
token: string;
|
||||
to: string;
|
||||
|
|
@ -21,12 +24,12 @@ const sendSignalMessageTask = async ({
|
|||
quoteAuthor,
|
||||
quoteTimestamp,
|
||||
}: SendSignalMessageTaskOptions): Promise<void> => {
|
||||
console.log(`[send-signal-message] Processing outgoing message:`, {
|
||||
logger.debug({
|
||||
token,
|
||||
to,
|
||||
conversationId,
|
||||
messageLength: message?.length,
|
||||
});
|
||||
}, 'Processing outgoing message');
|
||||
const bot = await db
|
||||
.selectFrom("SignalBot")
|
||||
.selectAll()
|
||||
|
|
@ -55,12 +58,12 @@ const sendSignalMessageTask = async ({
|
|||
const isGroupId = isUUID || isGroupPrefix || isBase64;
|
||||
const enableAutoGroups = process.env.BRIDGE_SIGNAL_AUTO_GROUPS === "true";
|
||||
|
||||
console.log(`[send-signal-message] Recipient analysis:`, {
|
||||
logger.debug({
|
||||
to,
|
||||
isGroupId,
|
||||
enableAutoGroups,
|
||||
shouldCreateGroup: enableAutoGroups && !isGroupId && to && conversationId,
|
||||
});
|
||||
}, 'Recipient analysis');
|
||||
|
||||
// If sending to a phone number and auto-groups is enabled, create a group first
|
||||
if (enableAutoGroups && !isGroupId && to && conversationId) {
|
||||
|
|
@ -90,9 +93,7 @@ const sendSignalMessageTask = async ({
|
|||
const createdGroup = groups.find((g) => g.id === finalTo);
|
||||
if (createdGroup && createdGroup.internalId) {
|
||||
internalId = createdGroup.internalId;
|
||||
console.log(
|
||||
`[send-signal-message] Got actual internalId: ${internalId}`,
|
||||
);
|
||||
logger.debug({ internalId }, 'Got actual internalId');
|
||||
} else {
|
||||
// Fallback: extract base64 part from ID
|
||||
if (finalTo.startsWith("group.")) {
|
||||
|
|
@ -100,22 +101,20 @@ const sendSignalMessageTask = async ({
|
|||
}
|
||||
}
|
||||
} catch (fetchError) {
|
||||
console.log(
|
||||
`[send-signal-message] Could not fetch group details, using ID base64 part`,
|
||||
);
|
||||
logger.debug('Could not fetch group details, using ID base64 part');
|
||||
// Fallback: extract base64 part from ID
|
||||
if (finalTo.startsWith("group.")) {
|
||||
internalId = finalTo.substring(6);
|
||||
}
|
||||
}
|
||||
console.log(`[send-signal-message] Created new Signal group:`, {
|
||||
logger.debug({
|
||||
groupId: finalTo,
|
||||
internalId,
|
||||
groupName,
|
||||
conversationId,
|
||||
originalRecipient: to,
|
||||
botNumber: bot.phoneNumber,
|
||||
});
|
||||
}, 'Created new Signal group');
|
||||
|
||||
// Notify Zammad about the new group ID via webhook
|
||||
await worker.addJob("common/notify-webhooks", {
|
||||
|
|
@ -131,23 +130,23 @@ const sendSignalMessageTask = async ({
|
|||
});
|
||||
}
|
||||
} catch (groupError) {
|
||||
console.error("[send-signal-message] Error creating Signal group:", {
|
||||
logger.error({
|
||||
error: groupError instanceof Error ? groupError.message : groupError,
|
||||
to,
|
||||
conversationId,
|
||||
});
|
||||
}, 'Error creating Signal group');
|
||||
// Continue with original recipient if group creation fails
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[send-signal-message] Sending message via API:`, {
|
||||
logger.debug({
|
||||
fromNumber: number,
|
||||
toRecipient: finalTo,
|
||||
originalTo: to,
|
||||
recipientChanged: to !== finalTo,
|
||||
groupCreated,
|
||||
isGroupRecipient: finalTo.startsWith("group."),
|
||||
});
|
||||
}, 'Sending message via API');
|
||||
|
||||
// Build the message data with optional quote parameters
|
||||
const messageData: signalApi.ApiSendMessageV2 = {
|
||||
|
|
@ -156,12 +155,12 @@ const sendSignalMessageTask = async ({
|
|||
message,
|
||||
};
|
||||
|
||||
console.log(`[send-signal-message] Message data being sent:`, {
|
||||
logger.debug({
|
||||
number,
|
||||
recipients: [finalTo],
|
||||
message: message.substring(0, 50) + "...",
|
||||
hasQuoteParams: !!(quoteMessage && quoteAuthor && quoteTimestamp),
|
||||
});
|
||||
}, 'Message data being sent');
|
||||
|
||||
// Add quote parameters if all are provided
|
||||
if (quoteMessage && quoteAuthor && quoteTimestamp) {
|
||||
|
|
@ -169,28 +168,28 @@ const sendSignalMessageTask = async ({
|
|||
messageData.quoteAuthor = quoteAuthor;
|
||||
messageData.quoteMessage = quoteMessage;
|
||||
|
||||
console.log(`[send-signal-message] Including quote in message:`, {
|
||||
logger.debug({
|
||||
quoteAuthor,
|
||||
quoteMessage: quoteMessage.substring(0, 50) + "...",
|
||||
quoteTimestamp,
|
||||
});
|
||||
}, 'Including quote in message');
|
||||
}
|
||||
|
||||
const response = await messagesClient.v2SendPost({
|
||||
data: messageData,
|
||||
});
|
||||
|
||||
console.log(`[send-signal-message] Message sent successfully:`, {
|
||||
logger.debug({
|
||||
to: finalTo,
|
||||
groupCreated,
|
||||
response: response?.timestamp || "no timestamp",
|
||||
});
|
||||
}, 'Message sent successfully');
|
||||
} catch (error: any) {
|
||||
// Try to get the actual error message from the response
|
||||
if (error.response) {
|
||||
try {
|
||||
const errorBody = await error.response.text();
|
||||
console.error(`[send-signal-message] Signal API error:`, {
|
||||
logger.error({
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
body: errorBody,
|
||||
|
|
@ -200,12 +199,12 @@ const sendSignalMessageTask = async ({
|
|||
toRecipients: [finalTo],
|
||||
hasQuote: !!quoteMessage,
|
||||
},
|
||||
});
|
||||
}, 'Signal API error');
|
||||
} 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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import { withDb, AppDatabase } from "../../lib/db.js";
|
|||
import { twilioClientFor } from "../../lib/common.js";
|
||||
import { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
|
||||
import workerUtils from "../../lib/utils.js";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
|
||||
const logger = createLogger('bridge-worker-twilio-recording');
|
||||
|
||||
interface WebhookPayload {
|
||||
startTime: string;
|
||||
|
|
@ -20,7 +23,7 @@ const getTwilioRecording = async (url: string) => {
|
|||
const { payload } = await Wreck.get(url);
|
||||
return { recording: payload as Buffer };
|
||||
} catch (error: any) {
|
||||
console.error(error.output);
|
||||
logger.error(error.output);
|
||||
return { error: error.output };
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { db } from "@link-stack/bridge-common";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
|
||||
const logger = createLogger('bridge-worker-send-whatsapp-message');
|
||||
|
||||
interface SendWhatsappMessageTaskOptions {
|
||||
token: string;
|
||||
|
|
@ -26,7 +29,7 @@ const sendWhatsappMessageTask = async ({
|
|||
body: JSON.stringify(params),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error({ error });
|
||||
logger.error({ error });
|
||||
throw new Error("Failed to send message");
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import Google from "next-auth/providers/google";
|
|||
import Apple from "next-auth/providers/apple";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import { checkAuth } from "./opensearch";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
|
||||
const logger = createLogger('leafcutter-auth');
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
pages: {
|
||||
|
|
@ -45,7 +48,7 @@ export const authOptions: NextAuthOptions = {
|
|||
|
||||
return user;
|
||||
} catch (e) {
|
||||
console.error({ e });
|
||||
logger.error({ e });
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
/* eslint-disable no-underscore-dangle */
|
||||
import { Client } from "@opensearch-project/opensearch";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
|
||||
const logger = createLogger('leafcutter-opensearch');
|
||||
|
||||
/* Common */
|
||||
|
||||
|
|
@ -302,7 +305,7 @@ export const updateUserVisualization = async (
|
|||
body,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error({ e });
|
||||
logger.error({ e });
|
||||
}
|
||||
|
||||
return id;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import { Client } from "@opensearch-project/opensearch";
|
|||
import { v4 as uuid } from "uuid";
|
||||
import taxonomy from "app/_config/taxonomy.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) => {
|
||||
const { tickets } = await req.json();
|
||||
|
|
@ -46,7 +49,7 @@ export const POST = async (req: NextRequest) => {
|
|||
});
|
||||
succeeded.push(id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
logger.error(e);
|
||||
failed.push(id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@link-stack/leafcutter-ui": "*",
|
||||
"@link-stack/logger": "*",
|
||||
"@link-stack/opensearch-common": "*",
|
||||
"@mui/icons-material": "^6",
|
||||
"@mui/material": "^6",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { createProxyMiddleware } from "http-proxy-middleware";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
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) {
|
||||
console.info("Has Signature");
|
||||
logger.info("Has Signature");
|
||||
}
|
||||
|
||||
if (referrerSignature && isResourcePath) {
|
||||
console.info("Has Signature");
|
||||
logger.info("Has Signature");
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 ?? "" };
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
5682
package-lock.json
generated
5682
package-lock.json
generated
File diff suppressed because it is too large
Load diff
19
package.json
19
package.json
|
|
@ -62,21 +62,10 @@
|
|||
"typescript": "latest"
|
||||
},
|
||||
"overrides": {
|
||||
"material-ui-popup-state": {
|
||||
"react": "$react"
|
||||
},
|
||||
"@chatscope/chat-ui-kit-react": {
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom"
|
||||
},
|
||||
"@mui/icons-material": {
|
||||
"react": "$react"
|
||||
},
|
||||
"mui-chips-input": {
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom",
|
||||
"@types/react": "$@types/react"
|
||||
}
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"@types/react": "^19.0.12",
|
||||
"@mui/material": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=10",
|
||||
|
|
|
|||
36
packages/logger/package.json
Normal file
36
packages/logger/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
56
packages/logger/src/config.ts
Normal file
56
packages/logger/src/config.ts
Normal 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;
|
||||
};
|
||||
35
packages/logger/src/index.ts
Normal file
35
packages/logger/src/index.ts
Normal 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;
|
||||
21
packages/logger/tsconfig.json
Normal file
21
packages/logger/tsconfig.json
Normal 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"]
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
/* eslint-disable no-underscore-dangle */
|
||||
import { Client } from "@opensearch-project/opensearch";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
|
||||
const logger = createLogger('opensearch-common-opensearch');
|
||||
|
||||
/* Common */
|
||||
|
||||
|
|
@ -284,7 +287,7 @@ export const updateUserVisualization = async (
|
|||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error({ e });
|
||||
logger.error({ e });
|
||||
}
|
||||
|
||||
return id;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"build": "tsc -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@link-stack/logger": "*",
|
||||
"@opensearch-project/opensearch": "^3.4.0",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { promises as fs } from "fs";
|
|||
import { glob } from "glob";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { createLogger } from "@link-stack/logger";
|
||||
|
||||
const logger = createLogger('zammad-addon-build');
|
||||
|
||||
const packageFile = async (actualPath: string): Promise<any> => {
|
||||
console.info(`Packaging: ${actualPath}`);
|
||||
logger.info({ actualPath }, 'Packaging file');
|
||||
const packagePath = actualPath.slice(4);
|
||||
const data = await fs.readFile(actualPath, "utf-8");
|
||||
const content = Buffer.from(data, "utf-8").toString("base64");
|
||||
|
|
@ -74,10 +77,10 @@ export const createZPM = async ({
|
|||
|
||||
for (const file of files) {
|
||||
await fs.unlink(file);
|
||||
console.info(`${file} was deleted`);
|
||||
logger.info({ file }, 'File was deleted');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
logger.error({ error: err }, 'Error removing old addon files');
|
||||
}
|
||||
await fs.writeFile(
|
||||
`../../docker/zammad/addons/${name}-v${version}.zpm`,
|
||||
|
|
@ -89,7 +92,7 @@ export const createZPM = async ({
|
|||
const main = async () => {
|
||||
const packageJSON = JSON.parse(await fs.readFile("./package.json", "utf-8"));
|
||||
const { name: fullName, displayName, version } = packageJSON;
|
||||
console.info(`Building addon ${displayName} v${version}`);
|
||||
logger.info({ displayName, version }, 'Building addon');
|
||||
const name = fullName.split("/").pop();
|
||||
await createZPM({ name, displayName, version });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"author": "",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@link-stack/logger": "*",
|
||||
"glob": "^11.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue