In web application development, server-side logging is an important concept to get right. Great server logging helps developers quickly fix bugs and tends to enhance an application's overall reliability. This contributes to application observability, something that software teams are often working to improve.

JavaScript logging is a crucial component of modern web application development and enables developers to create more reliable and secure applications. In this article, you'll learn the fundamentals of JavaScript logging—including what makes it so important—and also some practical tips on implementing server logging in your application for better reliability.

What is server-side logging?

Server-side logging refers to the process of recording events, errors, and other relevant information about the performance of an application on the server side of the application. Logging can be implemented in a few ways but is only useful if a developer can view the logs at a later time. The information collected from logs can be used to debug production application problems, evaluate system performance, and most importantly, track user behavior.

In addition to console logging, file logging and database logging can be used on the server side as an alternate way to persist information. The choice of technique depends on the requirements of your particular application. Some use cases may even call for third-party tools like Honeybadger Insights to ingest application logs and make them more useful.

The importance of server-side logging

An important benefit of logging things as they happen in a software system is how it empowers developers to detect bugs quickly. Just as important as the ability to find a bug, good server-side logging helps a software engineer diagnose where an error is coming from in the application code. Thus, this visibility into application behavior helps developers quickly detect software faults. With a good logging implementation, developers can quickly locate the precise mistake and take the necessary corrective action by reporting errors or fixing the mistake.

Server-side logging can also enhance an application's performance. Developers can find areas that need optimization and decide how to improve the performance of the program by logging performance statistics, such as response times and server load. Without visibility into an application, there's no way to find and fix bottlenecks.

Approaches to server-side JavaScript logging

Server-side JavaScript logging can be accomplished in a variety of ways, such as using built-in console methods from JavaScript, leaning on custom logging libraries, or introducing third-party logging libraries. Each option has its uses, and most applications use some combination of the few types that best fit their use case. An application early in development that's only used by a few users might be served well by simple built-in console methods, but as an application grows so do its needs for greater observability. Let's look into some options.

Built-in console methods

There are many built-in console JavaScript logging methods that work on both the browser and server side. You're probably already familiar with console.log(), but did you know about console.info, console.error(), and console.warn()? These other built-in JS logging methods provide a mechanism to categorize logs for easier analysis. These four methods can be used for different applications that suit the software application requirements. Here are some examples:

console.log("This is an informational message.");
console.info("This is an informational message");
console.warn("This is a warning message.");
console.error("This is an error message.");

This logging technique, using built-in methods, has some limitations, but it is very effective for quick and straightforward logging. Console messages, for example, are only visible on the server or in the developer tool of the browser, which may not be a good choice for logging in production. It's technically challenging to get directly to server logs, but also presents access problems for businesses with strict regulatory requirements around server access.

Custom logging libraries

Creating custom log functions is oftentimes a better solution to the limitations of built-in console methods. Many developers choose this option because of its flexibility. Custom logging libraries provide various features, such as:

  • Logging to files and databases directly (we'll talk more about this later)
  • Formatting messages
  • Filtering logs based on different criteria

Here's an example of creating a very simple custom logging library in Node.js that logs an error message to the console but doesn't throw an exception. This library is just a single function that adds extra functionality to console.log() and saves the result to a file:

const fs = require("fs");

function log(message) {
  const timestamp = new Date().toISOString();
  const logLine = `${timestamp} ${message}\n`;

  fs.appendFile("server.log", logLine, err => {
    if (err) {
      console.error("Error writing to log file: ", err);
    }
  });
}

log("Hello, world!");

This code above creates a log() function that saves a time-stamped message to a file called server.log. The appendFile()method is used to append log lines to the file, and the if (err) code block is used to handle errors saving the string to that file.

Although they can be very effective tools, custom logging libraries demand more development time than the built-in function technique. This example is straightforward, but custom solutions will often grow and certainly require more maintenance over time. The importance of keeping custom JavaScript logging code error-free cannot be overstated. Unreliable logging solutions only serve to compound production problems, making them harder to diagnose and address.

Without directing the logs somewhere like a file or database, a custom logging implementation will still have some shortcomings. Later in the article, we'll talk a bit about sending these logs to a file or database, but you can also opt to send logs to a third-party service for ingestion.

Third-party services

Another approach to server-side logging is using third-party services, such as Loggly, Papertrail, AWS CloudWatch, or Honeybadger Insights. These services provide centralized logging that has the ability to ingest data from multiple servers and across several applications while often providing additional features. The need for a team to use log analysis, alerting, filtering, and even custom graphs are all great reasons to lean on a third-party logging service.

Using a third-party service can simplify logging and provide more advanced features, but it may also introduce additional costs and complexity. Logging in JavaScript is easy, especially when you're just running console.log(). Third-party services require you to make API calls, whether directly or abstractly through a logging framework or library.

Here's an example of using a third-party logging service, such as Loggly:

const winston = require("winston");
const { Loggly } = require("winston-loggly");

const logger = winston.createLogger({
  level: "info",
  transports: [
    new Loggly({
      token: "YOUR_LOGGLY_TOKEN",
      subdomain: "YOUR_LOGGLY_SUBDOMAIN",
      tags: ["web", "logging"],
      json: true
    })
  ]
});

logger.info("Hello, world!");

The code above uses the Winston library to create logger objects that send log messages to Loggly. Then, Loggly transports log messages to the Loggly service, and the tag option is used to label log messages.

Using a third-party service for server logging introduces direct financial costs, as the service provider will charge some fee for logs sent. Still, many teams decide this cost is a reasonable tradeoff when considering the features that come with them.

Implementing server-side JavaScript logging

Now that we have covered the fundamentals of server-side JavaScript logging, let's look at how to implement these techniques in a web application.

Console logging

Console logging is a simple and effective mechanism for server-side logging. It involves writing messages to the server console, where they may be seen and analyzed by developers.

The built-in console object in Node.js can be used to implement console logging in your application. The console object offers a number of methods, such as console.log(), console.error(), and console.warn, for writing log messages (). These techniques let you compose console messages with varying degrees of severity, which can aid in sorting and prioritizing log messages.

For example, the following code snippet shows how to log an error message to the console using the built-in console method:

console.error('An error occurred: ', error);

Understanding JavaScript log levels

You probably use logging in JavaScript in a number of different use cases. Not all logs are created equal. We've already discussed the four built-in JavaScript log methods:

console.log("This is an informational message.");
console.info("This is an informational message");
console.warn("This is a warning message.");
console.error("This is an error message.");

Like other programming languages, JavaScript provides programmers with this simple way to differentiate logs. While console.log() simply logs the provided string in plain text, the higher JavaScript log levels provide ways for your tooling to color code, sort, or filter logs.

File logging

Another common logging method, called file logging, includes writing log messages to a file on the server. File logging is very helpful when you need to store a lot of log data or when you wish to keep log data for long durations. Sending a JavaScript log to a file adds surprisingly little complexity to an application when compared with just using console.log().

You can use a logging library, such as Winston or Bunyan, to integrate file logging into your program. These JavaScript packages give software developers a quick and effective interface to record log messages to various files.

For a quick example, the following code snippet demonstrates how to use Winston to write a log message to a file:

const winston = require('winston');

const logger = winston.createLogger({
  transports: [
    new winston.transports.File({ filename: 'app.log' })
  ]
});

logger.info('New user registered');

In this example, we're creating a logger instance using Winston and configuring it to write log messages to a file named app.log. We then use the logger instance to write an information message to the log. Winston comes with some helpful features, including seven different log levels to choose from. We used the info log level in this code example, which is the second lowest log level in the Winston package.

Database logging

With database logging, log messages and their associated metadata are persisted long-term in a database. This method is quite helpful when you need to save log data for a long period or when you wish to run complex queries on the log data. Database persistence is useful for querying across massive quantities in logs, while also storing logs in a semi-permanent way that allows for compliance with data retention policies.

You can utilize a database management system (DBMS) like MySQL, MongoDB, or PostgreSQL to integrate database logging into your application. This requires that your application has write access to a database, which is likely to be the case already if you're interested in server-side JS logging. You will first need to create a log table in the database and use SQL queries to insert log messages into the table.

For example, the following code snippet demonstrates how to use a MySQL database to log messages:

const mysql = require('mysql');

const connection = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'logs'
});

connection.connect();

const message = 'A new user has registered';
const timestamp = new Date();

connection.query('INSERT INTO log (message, timestamp) VALUES (?, ?)', [message, timestamp], (error, results) => {
  if (error) throw error;
  console.log('Log message inserted: ', message);
});

connection.end();

In this example, we're creating a MySQL database connection and using it to insert a log message into a log table. We're using SQL queries to insert the message and a timestamp into the table and then logging a message to the console to confirm that the message has been inserted.

Understanding log output

When implementing server-side JavaScript logging, interpreting and analyzing the log output effectively is one of the main goals. Well-structured logs can provide valuable insights into your application's behavior, performance, and potential issues. Here are some important things to keep in mind when formatting your log output.

Log format and structure

Consistent log formatting is essential for easy parsing and analysis. A great log entry should include, at a minimum:

  • Log timestamp: The exact time when the log was created
  • Log level: Indicates the severity or importance of the log (one of the types we discussed earlier such as info, warn, and error)
  • Log message: The actual content of the log
  • Log context: Additional relevant information (e.g., user ID, request ID, function name)

For example, a well-structured log entry might loo something like this:

2024-08-17T14:30:25.123Z [INFO] User redirected to homepage (user-id: 7732, request-id: 843AYN)

JavaScript logging output with proper formatting JavaScript logging output with proper formatting

Log aggregation and visualization

As your application grows, manually sifting through log files becomes impractical. Log aggregation tools can help collect, process, and visualize logs from multiple sources. Popular options such as Elasticsearch, Logstash, Splunk, and Honeybadger Insights allow you to search, filter, and create dashboards with your log data. These sorts of visualizations make it easier to spot trends and anomalies that you otherwise wouldn't have visibility into.

Log retention and rotation

Consider implementing log rotation to manage file sizes and ensure that your logs don't consume excessive disk space. It's a bit tricky to define a log retention policy that balances storage costs with the need for historical data, but addressing it early is easier than waiting until it becomes a problem.

Best practices for server-side JavaScript logging

By now you're probably already convinced that effective logging is crucial for maintaining and troubleshooting server-side JavaScript applications. Still, what does it mean for logging to be effective? By consistently following some of these JavaScript logging best practices, you can ensure that your logs are manageable and useful when you need them most.

Use structured logging

Structured logging requires formatting log entries as objects rather than plain text. You've already seen some logging libraries that make this easier, but it can be done with vanilla JavaScript. Consider using a JSON format for your logs, even if you're just using console.log(). Here's an example:

const log = {
  timestamp: new Date().toISOString(),
  level: 'INFO',
  message: 'Password reset email sent',
  user_id: 75274,
};

console.log(JSON.stringify(log));

When evaluating a 3rd-party logging service, make sure it supports structured logging. For example, Honeybadger Insights uses structured logging by default:

Honeybadger.event('Password reset email sent', {
  user_id: 75274,
  level: 'INFO',
});

Include contextual information

Always include relevant context in your log entries. The code sample above for structured logging is a great example of some helpful information to include in a log. Some other useful pieces of context that could be useful to include are:

  • Method names
  • Request IDs
  • User IDs
  • Relevant variables or parameters

Contextual information helps you quickly understand the circumstances surrounding a log entry, making debugging much more efficient.

Use appropriate log levels

Utilize different log levels to categorize the severity and importance of log messages. Earlier in the article you learned about the common log levels: error, warn, info, and debug.

Consistently following the conventions helps you differentiate logs that contain critical issues from those that have general operational information. Using appropriate log levels allows you to filter logs based on their importance and helps manage log volume.

Avoid logging sensitive information

Be cautious about what you log. Never include sensitive information like passwords or API keys. It's just as important to avoid logging sensitive information about users when not necessary. Personally identifiable information, health information, or financial data are all very valuable to malicious actors.

Avoiding logging sensitive information protects your users and your application by minimizing the vulnerable surface area for a cyber attack. Writing some shared code that you use to log things through gives you an opportunity to write filters to redact sensitive information that might slip through. If you must log sensitive data for debugging purposes, ensure it's properly masked or encrypted.

Test your logging

Ensure that your logging doesn't break under various conditions. Write tests for your logging code, especially for error scenarios. This helps prevent situations where errors in your logging code could mask the actual issues you're trying to debug.

These best practices help you create a robust logging system that provides valuable insights into your server-side JavaScript application's behavior, aids in debugging, and helps maintain the overall health of your system. Remember, effective logging is an ongoing process that requires regular review and optimization to ensure it continues to meet the evolving needs of your application and development team.

Advanced logging techniques

As your server-side JavaScript applications evolve, you may need to implement more advanced logging techniques to meet complex requirements and enhance your debugging capabilities. Here are some advanced logging approaches to consider.

Distributed tracing

If your application exists as part of a larger microservices architecture or distributed system, tracing requests across multiple services is absolutely necessary. Implement distributed tracing to track the flow of requests through your system. Open-source software such as OpenTelemetry can help you implement distributed tracing, allowing you to visualize the path of a request through your system.

Context logging

As you seek to add more and more contextual information to your logs, you may find yourself repeating common pieces of context like user IDs. If you're using a logging framework like Winston, you can inject context into the logs in one place. This approach allows you to automatically include request-specific information in all log messages within a request lifecycle.

Dynamic log levels

Applications usually have a log level set for their environment, which tells the application which logs it can ignore. This is useful to avoid logging useless development information in production but can result in a production application not logging enough information to diagnose a problem.

Many teams solve this by temporarily deploying a configuration change to change the level of logs being ingested, but this adds significant delay to actually identifying the source of a problem. You can implement dynamic log levels that can be adjusted at runtime without restarting your application, usually through an API endpoint.

This can be particularly useful for debugging production issues, letting your team adjust the level of logs they receive with just an API call instead of a deployment.

As always, remember to balance the benefits of detailed logging with performance considerations and data privacy requirements when implementing these techniques.

Take your logs to the next level

Server-side logging is an essential component of modern web development in any language. By following logging best practices, teams gain the power to quickly identify issues and even gain valuable insights into user behavior.

Throughout this article, we've explored various logging methods, from simple method calls to more advanced techniques like file and database logging. We've also discussed best practices that can help you create more effective and efficient logs, such as using appropriate log levels, structuring your log data, and being mindful of sensitive information.

However, implementing a comprehensive JavaScript logging strategy is just the beginning. To truly harness the power of your logs, you need tools that can help you analyze and derive actionable insights from the wealth of data you're collecting. Honeybadger Insights transforms your logs into valuable, actionable insights. Try Honeybadger Insights today!

author photo
Salem Olorundare

Salem is a software engineer who specializes in JavaScript. He enjoys breaking down advanced technical topics in his writing so they can be understood by people of all skill levels.

More articles by Salem Olorundare