From 02a9334bf481e9c9bd3f6bdf0613cdf5173ef0b0 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Fri, 19 Sep 2025 09:59:28 +0200 Subject: [PATCH 1/2] Add npm version badge to README for improved visibility --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7350a86..cbc9602 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # @xtr-dev/payload-mailing +[![npm version](https://img.shields.io/npm/v/@xtr-dev/payload-mailing.svg)](https://www.npmjs.com/package/@xtr-dev/payload-mailing) + A template-based email system with scheduling and job processing for PayloadCMS 3.x. ⚠️ **Pre-release Warning**: This package is currently in active development (v0.0.x). Breaking changes may occur before v1.0.0. Not recommended for production use. From 2f46dde53235f7768a1b0fd238278e30a19a7e9d Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sat, 20 Sep 2025 18:57:18 +0200 Subject: [PATCH 2/2] Add configurable logger with PAYLOAD_MAILING_LOG_LEVEL support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created centralized logger utility using Payload's built-in logger system - Added PAYLOAD_MAILING_LOG_LEVEL environment variable for log level configuration - Replaced all console.log/error/warn calls with structured logger - Added debug logging for immediate processing flow to help troubleshoot issues - Improved logging context with specific prefixes (IMMEDIATE, PROCESSOR, JOB_SCHEDULER, etc.) - Bumped version to 0.4.10 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- package.json | 2 +- src/collections/Emails.ts | 4 ++- src/jobs/processEmailsTask.ts | 7 +++-- src/sendEmail.ts | 16 ++++++++++-- src/utils/emailProcessor.ts | 11 +++++++- src/utils/jobScheduler.ts | 26 ++++++++++++++++--- src/utils/logger.ts | 48 +++++++++++++++++++++++++++++++++++ 7 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 src/utils/logger.ts diff --git a/package.json b/package.json index f159ae4..9a35b9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/payload-mailing", - "version": "0.4.9", + "version": "0.4.10", "description": "Template-based email system with scheduling and job processing for PayloadCMS", "type": "module", "main": "dist/index.js", diff --git a/src/collections/Emails.ts b/src/collections/Emails.ts index 0c2acbb..9bff505 100644 --- a/src/collections/Emails.ts +++ b/src/collections/Emails.ts @@ -1,5 +1,6 @@ import type { CollectionConfig } from 'payload' import { findExistingJobs, ensureEmailJob, updateEmailJobRelationship } from '../utils/jobScheduler.js' +import { createContextLogger } from '../utils/logger.js' const Emails: CollectionConfig = { slug: 'emails', @@ -220,7 +221,8 @@ const Emails: CollectionConfig = { } } catch (error) { // Log error but don't throw - we don't want to fail the email operation - console.error(`Failed to ensure job for email ${doc.id}:`, error) + const logger = createContextLogger(req.payload, 'EMAILS_HOOK') + logger.error(`Failed to ensure job for email ${doc.id}:`, error) } } ] diff --git a/src/jobs/processEmailsTask.ts b/src/jobs/processEmailsTask.ts index 92637db..12ddd0c 100644 --- a/src/jobs/processEmailsTask.ts +++ b/src/jobs/processEmailsTask.ts @@ -1,5 +1,6 @@ import type { PayloadRequest, Payload } from 'payload' import { processAllEmails } from '../utils/emailProcessor.js' +import { createContextLogger } from '../utils/logger.js' /** * Data passed to the process emails task @@ -67,7 +68,8 @@ export const scheduleEmailsJob = async ( delay?: number ) => { if (!payload.jobs) { - console.warn('PayloadCMS jobs not configured - emails will not be processed automatically') + const logger = createContextLogger(payload, 'SCHEDULER') + logger.warn('PayloadCMS jobs not configured - emails will not be processed automatically') return } @@ -79,6 +81,7 @@ export const scheduleEmailsJob = async ( waitUntil: delay ? new Date(Date.now() + delay) : undefined, } as any) } catch (error) { - console.error('Failed to schedule email processing job:', error) + const logger = createContextLogger(payload, 'SCHEDULER') + logger.error('Failed to schedule email processing job:', error) } } diff --git a/src/sendEmail.ts b/src/sendEmail.ts index be8292d..9d86e96 100644 --- a/src/sendEmail.ts +++ b/src/sendEmail.ts @@ -2,6 +2,7 @@ import { Payload } from 'payload' import { getMailing, renderTemplate, parseAndValidateEmails, sanitizeFromName } from './utils/helpers.js' import { BaseEmailDocument } from './types/index.js' import { processJobById } from './utils/emailProcessor.js' +import { createContextLogger } from './utils/logger.js' // Options for sending emails export interface SendEmailOptions { @@ -137,6 +138,9 @@ export const sendEmail = async maxTotalTime) { @@ -172,17 +178,20 @@ export const sendEmail = async 0) { // Job found! Get the first job ID (should only be one for a new email) jobId = Array.isArray(emailWithJobs.jobs) ? String(emailWithJobs.jobs[0]) : String(emailWithJobs.jobs) + logger.info(`Found job ID: ${jobId}`) break } // Log on later attempts to help with debugging (reduced threshold) - if (attempt >= 2) { - console.log(`Waiting for job creation for email ${email.id}, attempt ${attempt + 1}/${maxAttempts}`) + if (attempt >= 1) { + logger.debug(`Waiting for job creation for email ${email.id}, attempt ${attempt + 1}/${maxAttempts}`) } } @@ -204,9 +213,12 @@ export const sendEmail = async { + const logger = createContextLogger(payload, 'PROCESSOR') + logger.debug(`Starting processJobById for job ${jobId}`) + if (!payload.jobs) { throw new Error('PayloadCMS jobs not configured - cannot process job immediately') } try { + logger.debug(`Running job ${jobId} with payload.jobs.run()`) + // Run a specific job by its ID (using where clause to find the job) - await payload.jobs.run({ + const result = await payload.jobs.run({ where: { id: { equals: jobId } } }) + + logger.info(`Job ${jobId} execution completed`, { result }) } catch (error) { + logger.error(`Job ${jobId} execution failed:`, error) throw new Error(`Failed to process job ${jobId}: ${String(error)}`) } } diff --git a/src/utils/jobScheduler.ts b/src/utils/jobScheduler.ts index b414a7d..373787b 100644 --- a/src/utils/jobScheduler.ts +++ b/src/utils/jobScheduler.ts @@ -1,4 +1,5 @@ import type { Payload } from 'payload' +import { createContextLogger } from './logger.js' /** * Finds existing processing jobs for an email @@ -47,11 +48,16 @@ export async function ensureEmailJob( const mailingContext = (payload as any).mailing const queueName = options?.queueName || mailingContext?.config?.queue || 'default' + const logger = createContextLogger(payload, 'JOB_SCHEDULER') + logger.debug(`Ensuring job for email ${normalizedEmailId}`) + logger.debug(`Queue: ${queueName}, scheduledAt: ${options?.scheduledAt || 'immediate'}`) + // First, optimistically try to create the job // If it fails due to uniqueness constraint, then check for existing jobs // This approach minimizes the race condition window try { + logger.debug(`Attempting to create new job for email ${normalizedEmailId}`) // Attempt to create job - rely on database constraints for duplicate prevention const job = await payload.jobs.queue({ queue: queueName, @@ -62,21 +68,32 @@ export async function ensureEmailJob( waitUntil: options?.scheduledAt ? new Date(options.scheduledAt) : undefined }) - console.log(`Auto-scheduled processing job ${job.id} for email ${normalizedEmailId}`) + logger.info(`Auto-scheduled processing job ${job.id} for email ${normalizedEmailId}`) + logger.debug(`Job details`, { + jobId: job.id, + emailId: normalizedEmailId, + scheduledAt: options?.scheduledAt || 'immediate', + task: job.task, + queue: job.queue + }) return { jobIds: [job.id], created: true } } catch (createError) { + logger.warn(`Job creation failed for email ${normalizedEmailId}: ${String(createError)}`) + // Job creation failed - likely due to duplicate constraint or system issue // Check if duplicate jobs exist (handles race condition where another process created job) const existingJobs = await findExistingJobs(payload, normalizedEmailId) + logger.debug(`Found ${existingJobs.totalDocs} existing jobs after creation failure`) + if (existingJobs.totalDocs > 0) { // Found existing jobs - return them (race condition handled successfully) - console.log(`Found existing jobs for email ${normalizedEmailId}: ${existingJobs.docs.map(j => j.id).join(', ')}`) + logger.info(`Using existing jobs for email ${normalizedEmailId}: ${existingJobs.docs.map(j => j.id).join(', ')}`) return { jobIds: existingJobs.docs.map(job => job.id), created: false @@ -92,6 +109,7 @@ export async function ensureEmailJob( if (isLikelyUniqueConstraint) { // This should not happen if our check above worked, but provide a clear error + logger.error(`Unique constraint violation but no existing jobs found for email ${normalizedEmailId}`) throw new Error( `Database uniqueness constraint violation for email ${normalizedEmailId}, but no existing jobs found. ` + `This indicates a potential data consistency issue. Original error: ${errorMessage}` @@ -99,6 +117,7 @@ export async function ensureEmailJob( } // Non-constraint related error + logger.error(`Non-constraint job creation error for email ${normalizedEmailId}: ${errorMessage}`) throw new Error(`Failed to create job for email ${normalizedEmailId}: ${errorMessage}`) } } @@ -134,7 +153,8 @@ export async function updateEmailJobRelationship( }) } catch (error) { const normalizedEmailId = String(emailId) - console.error(`Failed to update email ${normalizedEmailId} with job relationship:`, error) + const logger = createContextLogger(payload, 'JOB_SCHEDULER') + logger.error(`Failed to update email ${normalizedEmailId} with job relationship:`, error) throw error } } \ No newline at end of file diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..99174f0 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,48 @@ +import type { Payload } from 'payload' + +let pluginLogger: any = null + +/** + * Get or create the plugin logger instance + * Uses PAYLOAD_MAILING_LOG_LEVEL environment variable to configure log level + * Defaults to 'info' if not set + */ +export function getPluginLogger(payload: Payload) { + if (!pluginLogger && payload.logger) { + const logLevel = process.env.PAYLOAD_MAILING_LOG_LEVEL || 'info' + + pluginLogger = payload.logger.child({ + level: logLevel, + plugin: '@xtr-dev/payload-mailing' + }) + + // Log the configured log level on first initialization + pluginLogger.info(`Logger initialized with level: ${logLevel}`) + } + + // Fallback to console if logger not available (shouldn't happen in normal operation) + if (!pluginLogger) { + return { + debug: (...args: any[]) => console.log('[MAILING DEBUG]', ...args), + info: (...args: any[]) => console.log('[MAILING INFO]', ...args), + warn: (...args: any[]) => console.warn('[MAILING WARN]', ...args), + error: (...args: any[]) => console.error('[MAILING ERROR]', ...args), + } + } + + return pluginLogger +} + +/** + * Create a context-specific logger for a particular operation + */ +export function createContextLogger(payload: Payload, context: string) { + const logger = getPluginLogger(payload) + + return { + debug: (message: string, ...args: any[]) => logger.debug(`[${context}] ${message}`, ...args), + info: (message: string, ...args: any[]) => logger.info(`[${context}] ${message}`, ...args), + warn: (message: string, ...args: any[]) => logger.warn(`[${context}] ${message}`, ...args), + error: (message: string, ...args: any[]) => logger.error(`[${context}] ${message}`, ...args), + } +} \ No newline at end of file