DEV Community

Cover image for Exploring customLogger: Smart, Customizable Logging in Payload CMS
seyed mojtaba shadab
seyed mojtaba shadab

Posted on

Exploring customLogger: Smart, Customizable Logging in Payload CMS

🧾 Brief Introduction to Payload CMS

Payload CMS is a Headless content management system based on Node.js and TypeScript, designed for developers. Unlike many traditional CMSs that come with pre-built UIs or themes, Payload focuses on providing a developer-centric, API-driven, and customizable experience.

🎯 Key Features of Payload:

  • TypeScript-first structure: All structures, data models, and configurations are defined with the powerful TypeScript type system.
  • Modern admin interface: Automatically generates a complete content management UI from your schema.
  • Built-in security and authentication: It has a built-in, extensible authentication and access control system.
  • Hooks and Plugins Support: You can control the behavior of the system precisely using hooks and plugins.

Payload allows developers to create a highly customizable, secure, and extensible content management system without being limited to predefined structures. That is why it is a popular choice for complex, custom, and modern projects.

πŸ” What is the payload-auditor plugin?

The payload-auditor plugin is a powerful plugin for Payload CMS that allows for precise audit of operations performed on collections. The plugin automatically records all changes such as create, update, and delete data and stores them in a structured log format.

✨ The main goal of this plugin:

Provide a transparent and reliable logging system for monitoring user behavior and reviewing database-level change history.

βš™οΈ Key features:

  • Automatic logging of operations on collections
  • Support for defining custom logger for custom logging
  • Support for all CMS payload hooks
  • Modular design with extensibility for complex projects

Using this plugin, developers and system administrators can easily understand who, when, and what changes were made to the system; a vital feature for projects where security, transparency, and audit trails are of high importance. To learn more about this plugin, you can read the article Tracking and Security in Payload CMS with the Payload-Auditor Plugin.

πŸ“š The importance of custom logging in content management systems (CMS)

In content management systems, especially in enterprise or sensitive projects, accurate tracking of user activities and data changes plays a very important role. While basic logging can capture general information about operations, custom logging allows for accurate, targeted, and analyzable information to be stored for the specific needs of each project.

Why do we need custom logging?

1. 🎯 Customization

Projects are different; one project may need to record user IP, another may want to store user roles or geographic information. Custom logging allows for critical information tailored to the project scenario to be recorded.

2. πŸ•΅οΈβ€β™€οΈ Transparency and Audit Trail

For systems that require user behavior to be investigated or security issues to be addressed, the complete change trail (who changed what, when, and how) must be clearly visible. This transparency cannot be achieved without accurate and customized logging.

3. πŸ” Security and Accountability

In many projects, especially in areas such as finance, medicine or government, accurate recording of activities plays an important role in data security and user accountability. Custom logs can also be used as legal evidence.

4. πŸ“Š Analytics and Intelligence

With rich and structured logs, you can analyze user behavior, identify process weaknesses and ultimately make more informed decisions to improve system performance.

Logging is not just a monitoring tool; it is an integral part of security, optimization and professional management of content systems. By providing the ability to record custom logs, you can have much more control and insight into what is happening in the system.


πŸ”§ What is customLogger?

In the payload-auditor plugin, the customLogger feature allows developers to fully customize logging behavior at the hook level. This feature is designed for projects that need to generate or modify log content in a specific way depending on different conditions, since it is almost impossible to predict the default values for logging in the logs.

🧩 Defining customLogger at the Hook Level

customLogger is an optional function that can be specified in the payload-auditor plugin configuration. This function is called when the hook used for each operation is executed and can edit or overwrite the log data before final storage.

customLogger?: (
  args: Parameters<AllCollectionHooks['afterChange']>[0],
  fields: Omit<AuditorLog, 'hook'>,
) => Omit<AuditorLog, 'hook'> | Promise<Omit<AuditorLog, 'hook'>>
Enter fullscreen mode Exit fullscreen mode

In this function:

  • args: Contains information about the hook for the same document and can have values like req, operation, doc, etc.
  • fields: Contains the default log structure without the hook field (because the principle of which hook created this log should not change).
  • The output of the function should be a final version of AuditorLog (except for the hook added by the plugin).

πŸ”€ Multi-level customLoggers

In payload-auditor, we have considered four levels of the customLogger function to increase flexibility.

πŸ”’ customLogger definition levels (in order of execution priority):

1. πŸ₯‡ Operation-Level

At this level, a dedicated customLogger can be defined for each specific operation (create, update, delete). This level has the highest execution priority and if it exists, no other levels will be executed.

create: {
  customLogger: (args, fields) => {
    return { ...fields, user: null }
  },
  enabled: true,
}
Enter fullscreen mode Exit fullscreen mode

πŸ“Œ Applies only to that specific operation
βœ… Has the highest priority


2. πŸ₯ˆ Hook-Level

If customLogger is not defined for a specific operation, the system looks for customLogger at the hook level, such as afterChange.

customLogger: (args, fields) => {
  return { ...fields, user: null }
}
Enter fullscreen mode Exit fullscreen mode

πŸ“Œ Only for specific hook (e.g. afterChange)
❌ Not executed if customLogger is defined as an operation level
βœ… Only works on active operations


3. πŸ₯‰ All Hooks Level (Hooks-Level)

At this level, customLogger is applied to all hooks and operations, if none of the previous two levels exist.

customLogger: (args, fields) => {
  return { ...fields, type: 'error' }
}
Enter fullscreen mode Exit fullscreen mode

πŸ“Œ Only runs if there is no logger at the hook and operation level
βœ… Only works on active operations
❌ Cannot be defined for a specific hook – covers the entire hook system


4. 🏁 Global-Level

If no customLogger is defined at the operation, hook or all hooks level, then the global customLogger is run. This level works for all collections.

customLogger: (args, fields) => {
  return { ...fields, userAgent: 'custom user agent' }
}
Enter fullscreen mode Exit fullscreen mode

πŸ“Œ Last level of fallback in execution priority
βœ… Works only on active operations


πŸ” Execution hierarchy summary:

[Highest priority] Operation β†’ Hook β†’ All hooks β†’ Global [Lowest priority]

Enter fullscreen mode Exit fullscreen mode

The more detailed the definition level, the higher the execution priority. This hierarchy provides great flexibility for precise, customized, and targeted logging.


🧬 customLogger structure

The customLogger function is the heart of the custom logging mechanism in the payload-auditor plugin. This function allows you to edit or generate log fields before final logging. To do this, the function takes two inputs and must return a specific output.

πŸ›  Function Inputs

The customLogger function has two main inputs:

1. args: All hook arguments

This argument contains complete information about the current hook and the operation that was executed. Typically contains values like:

  • req: HTTP request object
  • operation: Operation type (create, update, delete)
  • doc: Final document after modification
  • previousDoc: Previous document (in update and delete operations)
  • Other information depending on the hook type

2. fields: Initial log structure (Log Fields)

This object contains the default log data generated by the plugin, without fields hook. You can override, modify, or add fields to this object.

πŸ“Note: If you want to add a new field, you must define that field for the plugin root collection using configureRootCollection

πŸ“€ Expected Output

The customLogger function should return an object that has the following structure:

Omit<AuditorLog, 'hook'>
Enter fullscreen mode Exit fullscreen mode

That is, it must include all the fields required for logging, except hook which is automatically added by the plugin.

πŸ“Œ Simple example:

(args, fields) => {
return {
...fields,
user: null,
ip: args.req?.ip || 'unknown',
}
}
Enter fullscreen mode Exit fullscreen mode

⏳ Support for async operations

The customLogger function can also operate asynchronously (*****async* ) . That is, you can use async functions such as accessing a database, reading from external APIs or long-running processes and return the final value with await.

πŸ“Œ Example with Promise:

async (args, fields) => {
const geoInfo = await getGeoInfo(args.req?.ip)
return {
...fields,
location: geoInfo.city,
}
}
Enter fullscreen mode Exit fullscreen mode

πŸ”„ If the function output is of type Promise, the plugin will automatically resolve it.

This flexibility in the customLogger structure allows you to generate the structure you want for any scenario, from simple logging to the most complex analytics.


πŸ“Œ Practical examples

In this section, we will examine two common examples of customLogger usage; one simple and the other with async operations. Each example is analyzed line by line to make the practical concepts clear.

1. 🧼 Changing the user value to null

customLogger: (args, fields) => {
return { ...fields, user: null };
}
Enter fullscreen mode Exit fullscreen mode

🧩 Line-by-line analysis

  • customLogger: is a synchronous function that takes two parameters:
  • args: information about the operation, such as request, document, etc.
  • fields: raw log data before final processing.
  • ...fields: first we keep all the initial fields of the log.
  • user: null: then we manually set the value of user to null.

🎯 Real-world uses of this change

  • Removal of sensitive information: In some scenarios you may not want to record user IDs or information (for example, in anonymous forms).
  • Anonymization of logs for privacy or in test environments.
  • Automated processes where no human user is involved and user is irrelevant.

2. 🌐 Adding custom information (e.g. IP)

πŸ“Note: If you want to add a new field, you must define that field for the plugin root collection using configureRootCollection

customLogger: async (args, fields) => {
const ip = await getIPFromRequest(args.req);
return { ...fields, meta: { ...fields.meta, ip } };
}
Enter fullscreen mode Exit fullscreen mode

🧩 Line-by-line analysis

  • The function is async because it uses the await helper function.
  • getIPFromRequest(args.req): A hypothetical function to extract the IP from the request.
  • ...fields: Preserve the original data.
  • meta: { ...fields.meta, ip }: Add the IP to the log metadata section, without removing the previous metadata.

🧰 Introducing the helper function

The getIPFromRequest function can be defined simply as:

function getIPFromRequest(req?: PayloadRequest): string {
return req?.ip || req?.headers['x-forwarded-for']?.toString() || 'unknown';
}
Enter fullscreen mode Exit fullscreen mode

This function first gets the IP from req.ip, otherwise it uses proxy headers.

🎯 Real-world applications

  • Security analysis: Detect suspicious situations by examining IP.
  • Recording user behavior based on geographic location.
  • Monitoring system load from specific regions or specific users.

⚠️ Critical points when using customLogger

To use customLogger correctly and effectively in the payload-auditor plugin, it is crucial to be familiar with some limitations and prioritization behaviors. In this section, we will review the most important critical points:

πŸ”„ 1. Prioritization when using customLogger

If you have defined customLogger at different levels (for example, both at the operation level and at the hook or global level), the plugin will always use the closest definition. The order of precedence is as follows:

  1. customLogger at the operation-level For example:
create: {
customLogger: ...
}
Enter fullscreen mode Exit fullscreen mode

βœ… Highest priority – will only be executed for that specific operation.

  1. customLogger at hook level For example:
afterChange: {
customLogger: ...
}
Enter fullscreen mode Exit fullscreen mode

βœ… Runs only if the corresponding operation does not have a dedicated customLogger.

  1. customLogger at the global hooks level For example:
hooks: {
customLogger: ...
}
Enter fullscreen mode Exit fullscreen mode

βœ… Runs if neither operation-level nor hook-level customLogger exists.

  1. customLogger at global level (for all collections) For example:
auditor: {
customLogger: ...
}
Enter fullscreen mode Exit fullscreen mode

βœ… Finally, the final fallback; runs only if no other level has a customLogger.

🧩 2. Differences with customLogger at hook level

There are important differences between customLogger at hook level (e.g. afterChange) and operation level:

Feature customLogger (operation) customLogger (hook-level)
Dependent on operation type For any operation you have defined Yes, but only when not defined
Scope of effect A specific operation All operations related to that hook
Execution priority Higher Lower than operation level

⚠️ It is important not to confuse these two levels, as you might think your customLogger is running, but because there is a more precise version at a lower level, it is not called at all.


βœ… Best Practices

To make your logging system both fast and secure, it is very important to follow a few key principles in the design and implementation of customLogger. In this section, we will discuss recommendations that will both improve the developer experience and make the system easier to maintain:

🧼 1. Keep your log structure simple

  • Minimalism in logging helps reduce data volume and storage costs.
  • Avoid adding unnecessary or nested fields.
  • Always design logs for fast processing in analytics systems (like ELK, Datadog, or Logstash).

βœ… Good example:

return {
...fields,
action: 'create',
user: user.id,
};
Enter fullscreen mode Exit fullscreen mode

❌ Bad example:

return {
...fields,
debugDump: req.body,
requestHeaders: req.headers,
timestamp: new Date(),
callStack: new Error().stack,
};
Enter fullscreen mode Exit fullscreen mode

πŸ” 2. Avoid storing sensitive data (unless encrypted)

πŸ“Note: The plugin itself may store email in raw form in the logs. It is the developer's responsibility to use encryption to store sensitive data

  • Avoid storing private user data such as email, IP, or phone number in raw form.
  • If you need to store this data, use encryption or hashing.

βœ… Suggested example:

return {
...fields,
meta: {
emailHash: sha256(user.email),
},
};
Enter fullscreen mode Exit fullscreen mode

🌐 3. Use async for external data

If you need to get data from external sources (such as IP location, browser information, or external validations), customLogger fully supports async functions.

Example: Adding user location from IP

customLogger: async (args, fields) => {
const location = await getGeoLocation(args.req?.ip);
return {
...fields,
meta: {
...fields.meta,
location,
},
};
}
Enter fullscreen mode Exit fullscreen mode

πŸ“Œ Note: If you use external resources:

  • Pay attention to API latency.
  • Consider caching or rate-limiting.
  • Use appropriate timeouts so that the entire log process does not break.

🎯 Other recommendations

  • Use collections for testing customLogger whose changes are less critical.
  • Do not use customLogger to modify the original data (it is only for creating logs).
  • If you need a different customLogger based on the user type, you can put a condition in it:
if (args.req?.user?.role === 'admin') {
return { ...fields, severity: 'high' };
}
Enter fullscreen mode Exit fullscreen mode

πŸ“ Conclusion

The customLogger feature in the payload-auditor plugin is a very powerful tool for customizing the logging process in Payload CMS-based projects. This feature allows you to precisely and with full control, tailor the content of the logs to your project's security, analytical, and operational needs.

🧩 Summary of features

  • High flexibility at four levels: operation level, hook, hooks-level and global
  • Ability to process synchronously and asynchronously (async) in log generation
  • Support for external data such as IP, geolocation or user role
  • Ability to filter, modify, or remove sensitive information before saving

🎯 More accurate tracking of system changes

By using customLogger wisely, you can:

  • Record and analyze specific changes based on user context
  • Generate targeted and actionable logs that are valuable for monitoring and security tools
  • Make the debugging process faster and more accurate for the development team

πŸ’‘ Suggestions for developers

  • Use customLogger to build a reliable logging infrastructure alongside other observability tools like Sentry Or use Grafana.
  • For projects with high security requirements, be sure to encrypt sensitive data in logs.
  • Use async loggers to balance system performance with log richness.

Ultimately, customLogger is not just an optional feature, but can be a key part of your audit and security strategy. If used correctly, it can be the difference between a purely decorative log and a real analysis and response tool.

πŸ”— Link to the main plugin documentation

πŸ’» GitHub Repository Link

In this repo you can:

  • View and install the latest version of the plugin
  • Track Issues and Feature Requests
  • Contribute to the plugin development

🀝 Appreciation and Contribution

If you found this plugin useful or this article helped you understand it better, please support me by giving the payload-auditor repo a ⭐️. This motivates me to develop more open source tools.

πŸ™ Thank you for your support!

Top comments (0)