avatarIsrael Josué Parra Rosales

Summary

This context delves into the shared package of a microservices architecture, focusing on the error handling and logging mechanisms, and emphasizes the importance of these components for robust, secure, and maintainable applications.

Abstract

The provided text is part of a comprehensive guide on microservices architecture, specifically focusing on the shared package that encapsulates common utilities and functionalities. It breaks down the significance of proper error handling through the use of a custom TemplateError structure and predefined error response functions, which are crucial for enhancing user experience, security, and debugging. Additionally, the text underscores the role of structured logging using the logrus library, which is essential for monitoring, auditing, and maintaining microservices. The shared package is presented as a foundational element for consistency and reliability across the microservices ecosystem.

Opinions

  • Proper error handling is key to improving user experience by providing clear and actionable feedback in case of HTTP request errors.
  • Structured error responses are important for security, preventing the exposure of sensitive system information.
  • Detailed logs are invaluable for debugging, monitoring, and adhering to regulatory compliance standards.
  • A single shared logger instance is recommended for consistency and efficiency in logging practices.
  • The use of the logrus library with JSON formatting is advocated for advanced logging capabilities.
  • The shared package's approach to error handling and logging is seen as a best practice for building reliable and maintainable microservices.

Chapter 14 Coding our Microservice (Part 4)

Explaining the shared package

The following list is the previous chapters of this series:

I recommend you take a look at the previous chapters if you have not read them yet. That will help you to get more knowledge in this wonderful world of “Microservices architecture”.

Intruduction

In this chapter, we delve into a pivotal package of our microservice architecture, the “shared” package. This package serves as the cornerstone for shared utilities and functionalities that are indispensable across various layers of our microservices ecosystem.

We will explore two key sub-packages within the “shared” package, “error” and “logger.” Through meticulous examination, we’ll elucidate their significance, functionalities, and how they contribute to a robust and cohesive microservices infrastructure.

...
│   └── shared/
│       ├── error/
│       │    └── error.go
│       └── logger/
│           └── logger.go
...

Next, describe and analyze each one of the files defined under this package.

/shared/error/error.go

package error

import (
    "errors"
    "net/http"
)

var (
    RecordNotFound = errors.New("record not found")
)

type TemplateError struct {
    Status  int    `json:"status"`
    Message string `json:"msg"`
    Err     error  `json:"error"`
}

type errorResponse struct {
    Msg string `json:"msg"`
}

func (e TemplateError) Error() string {
    return e.Err.Error()
}

func NewRecordNotFoundError(msg string, err error) TemplateError {
    return TemplateError{
        Status:  http.StatusNotFound,
        Message: msg,
        Err:     err,
    }
}

func NewUnespectedError(msg string, err error) TemplateError {
    return TemplateError{
        Status:  http.StatusInternalServerError,
        Message: msg,
        Err:     err,
    }
}

func NewBadRequestError(msg string, err error) TemplateError {
    return TemplateError{
        Status:  http.StatusBadRequest,
        Message: msg,
        Err:     err,
    }
}

func NewValidationError(msg string, err error) TemplateError {
    return TemplateError{
        Status:  http.StatusBadRequest,
        Message: msg,
        Err:     err,
    }
}

func NewErrorResponse(message string) errorResponse {
    return errorResponse{
        Msg: message,
    }
}

Before describing the preceding code, it is important to mention why it is good practice to handle errors and return the correct response based on the following points:

User Experience When an HTTP request error occurs, such as a bad request or failed authentication, providing an appropriate response helps users understand and fix the problem, improving the user experience and the reliability of your application.

Security Handling errors properly prevents attackers from obtaining sensitive information about your application or system, such as database details or the internal structure of your application, since we can format errors by showing only the information needed to understand the error that occurred.

Debugging and Monitoring Proper error handling makes it easy to identify and fix problems in your application. Detailed logs and meaningful error responses are essential for monitoring and troubleshooting.

Compliance with rules and regulations Depending on your application context, you may need to comply with certain standards and regulations that require proper error handling.

In general, handling errors and giving adequate responses in a Gin-Gonic handler is essential to guarantee a reliable, secure, and maintainable application, in addition to improving the user experience.

Explaining the code: The provided code establishes a collection of error definitions and introduces a structured mechanism, TemplateError, for handling and responding to errors systematically.

This approach proves particularly valuable in web applications, where the delivery of precise HTTP responses in error scenarios is pivotal. The functions NewRecordNotFoundError, NewUnespectedError, NewBadRequestError, and NewValidationError serve the purpose of instantiating TemplateError objects, each assigned a distinct HTTP status code based on the nature of the error. This standardized error-handling approach promotes consistency throughout the application, facilitating the delivery of well-structured error responses.

For our use case and code example those functions are referenced on the repositories and handlers logic. On the repositories side, we are sending back the correct and formatted error, on the other hand, the helper function (previously analyzed) gets the error, after that validates the type of custom error, and then sets the correct response, adding the message and the response code.

/shared/logger/logger.go

package logger

import "github.com/sirupsen/logrus"

var logger *logrus.Logger

func NewLogger() *logrus.Logger {
    if logger == nil {
        logger = logrus.New()
        logger.SetFormatter(&logrus.JSONFormatter{})
    }
    return logger
}

Before delving into the details of the code found in logger.go, it’s crucial to emphasize the significance of logs in any application, particularly within the context of microservices. Logs play a multifaceted role, offering several key benefits, such as the following:

Debugging and Troubleshooting Logs serve as a vital tool for debugging and troubleshooting in microservices. When errors or unexpected behavior occur, logs act as a crucial resource for identifying and resolving issues. They offer essential insights into the microservice’s behavior, including the state of variables, input and output values, and any exceptions that were raised. These details are invaluable for pinpointing the root cause of problems and implementing effective solutions.

Audit Logs play a pivotal role in enabling comprehensive auditing of all actions executed within the microservice. This functionality is indispensable for adhering to regulatory compliance standards and conducting thorough security audits.

Monitoring and Alerts Logs are instrumental in continuous monitoring processes, as monitoring systems can scrutinize them for unusual patterns or critical errors. These systems trigger timely alerts upon detecting anomalies, enabling swift responses to resolve issues before they impact end users.

History Tracking LLogs provide a historical record of all operations performed on the microservice. This historical record proves invaluable for tracking past events, dissecting the root causes of issues, and maintaining a comprehensive activity history.

Documentation in Real-time Logs effectively serves as real-time documentation, providing an up-to-the-minute of the microservice’s activities. This proves particularly beneficial for developers who rely on a clear understanding of the service’s behavior during maintenance or debugging tasks.

Explaining the code: In the provided code, the logrus library is employed to handle logs within the application. It adopts a straightforward approach by creating a single application-wide shared log instance. The rationale behind this practice is to avoid the proliferation of multiple logging instances throughout the application. Utilizing a shared instance, named “logger” in this context, prevents resource duplication and ensures uniformity in logging practices across the entire application.

Now, let’s dive into a step-by-step explanation of the code.

In the provided code, the logrus library is employed to handle logs within the application. It adopts a straightforward approach by creating a single application-wide shared log instance. The rationale behind this practice is to avoid the proliferation of multiple logging instances throughout the application. Utilizing a shared instance, named “logger” in this context, it prevents resource duplication and ensures uniformity in logging practices across the entire application.

logrus import The “github.com/sirupsen/logrus” library is imported which provides the advanced logging functionality in JSON format.

Declares the “logger” variable This variable will be used to maintain a single shared instance of logging across the entire application.

“NewLogger” function This function is the one used to get an instance of the registry. It works in the following way:

  • Check if the “logger” variable has already been initialized. If it hasn’t, create a new log instance using “logrus.New()”.
  • Set the log format so that logs are generated in JSON format using “logger.SetFormatter(&logrus.JSONFormatter{})”.
  • Returns the logger variable.

The function and variable are initialized in “api.go” and passed to the components that need it.

Next readings …

Wait for Chapter 14 “Coding our Microservice (Part 5) — Implementing Unit Testing”.

Golang
Software Development
Software Architecture
Software Engineering
Computer Science
Recommended from ReadMedium